在我的代码中,我有一个循环,等待某个状态从其他线程更改。另一个线程可以工作,但是我的循环从未看到更改过的值。它永远等待。但是,当我在循环中放入System.out.println语句时,它突然起作用了!为什么?


下面是我的代码示例:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (pizzaArrived == false) {
            //System.out.println("waiting");
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        pizzaArrived = true;
    }
}


while循环运行时,我调用deliverPizza()从另一个线程设置pizzaArrived变量。但是,仅当取消注释System.out.println("waiting");语句时,该循环才起作用。发生了什么事?

#1 楼

允许JVM假定其他线程在循环期间不更改pizzaArrived变量。换句话说,它可以将pizzaArrived == false测试提升到循环外部,从而优化此操作:

while (pizzaArrived == false) {}


进入此操作:

if (pizzaArrived == false) while (true) {}


这是一个无限循环。

为确保一个线程所做的更改对其他线程可见,您必须始终在线程之间添加一些同步。最简单的方法是使共享变量volatile:使变量volatile可以确保不同的线程将看到彼此更改对其的影响。这样可以防止JVM缓存pizzaArrived的值或将测试提升到循环外部。相反,它必须每次都读取实变量的值。

(更正式地,volatile在访问变量之间创建事前关联关系。这意味着线程在进行传递之前所做的所有其他工作即使接收到披萨的线程也看不到披萨,即使这些其他更改不是volatile变量。)

同步方法主要用于实现互斥(防止同时发生两件事) ),但它们也具有与volatile相同的副作用。在读取和写入变量时使用它们是使更改对其他线程可见的另一种方法:

volatile boolean pizzaArrived = false;



打印语句的效果

System.outPrintStream对象。 PrintStream的方法是这样同步的:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        while (getPizzaArrived() == false) {}
        System.out.println("That was delicious!");
    }

    synchronized boolean getPizzaArrived() {
        return pizzaArrived;
    }

    synchronized void deliverPizza() {
        pizzaArrived = true;
    }
}


同步可防止pizzaArrived在循环期间被缓存。严格来说,两个线程必须在同一个对象上同步,以确保对变量的更改是可见的。 (例如,在设置println之后调用pizzaArrived,然后在读取pizzaArrived之前再次调用它是正确的。)如果只有一个线程在特定对象上同步,则允许JVM忽略它。实际上,JVM不够聪明,无法证明设置q​​4312079q后其他线程不会调用println,因此它假定它们可能会调用。因此,如果调用pizzaArrived,它将无法在循环期间缓存变量。这就是为什么这样的循环在具有打印语句时会起作用的原因,尽管它不是正确的解决方法。 ,当他们尝试调试其循环为何不起作用时!


更大的问题

System.out.println是一个忙等待循环。那很糟!等待时,它占用CPU,这会降低其他应用程序的速度,并增加系统的电源使用,温度和风扇速度。理想情况下,我们希望循环线程在等待时进入睡眠状态,这样它就不会占用CPU。

有一些方法可以做到这一点:

使用wait / notify

一种低级解决方案是使用System.out的等待/通知方法:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}


在此版本的代码中,循环线程调用while (pizzaArrived == false) {},这使线程进入睡眠状态。休眠时将不使用任何CPU周期。在第二个线程设置变量之后,它将调用Object唤醒正在等待该对象的所有线程。这就像让比萨饼人按门铃一样,这样您就可以在等待时坐下来休息,而不必笨拙地站在门口。

当在对象上调用wait / notify时,您必须持有该对象的同步锁,以上代码就是这样做的。您可以使用任何喜欢的对象,只要两个线程使用同一个对象即可:在这里,我使用了wait()notifyAll()的实例)。通常,两个线程将无法同时输入同一对象的同步块(这是同步目的的一部分),但是它在这里起作用,因为一个线程在this方法内部时会暂时释放同步锁。 >
BlockingQueue

MyHouse用于实现生产者-消费者队列。 “消费者”从队列的最前面取物品,“生产者”在队列的最后面取物品。示例:

class MyHouse {
    boolean pizzaArrived = false;

    void eatPizza() {
        synchronized (this) {
            while (!pizzaArrived) {
                try {
                    this.wait();
                } catch (InterruptedException e) {}
            }
        }

        System.out.println("That was delicious!");
    }

    void deliverPizza() {
        synchronized (this) {
            pizzaArrived = true;
            this.notifyAll();
        }
    }
}


注:wait()BlockingQueueput方法可以抛出take,这是必须处理的检查异常。在上面的代码中,为简单起见,重新抛出了异常。您可能希望在方法中捕获异常,然后重试put或take调用以确保成功。除了这一点之外,BlockingQueue非常易于使用。

这里不需要其他同步,因为InterruptedException确保在将项目放入队列之前,所有线程都可以看到线程这些项目就出来了。

执行器

BlockingQueue就像执行任务的现成BlockingQueue一样。示例:

class MyHouse {
    final BlockingQueue<Object> queue = new LinkedBlockingQueue<>();

    void eatFood() throws InterruptedException {
        // take next item from the queue (sleeps while waiting)
        Object food = queue.take();
        // and do something with it
        System.out.println("Eating: " + food);
    }

    void deliverPizza() throws InterruptedException {
        // in producer threads, we push items on to the queue.
        // if there is space in the queue we can return immediately;
        // the consumer thread(s) will get to it later
        queue.put("A delicious pizza");
    }
}


有关详细信息,请参阅ExecutorBlockingQueueExecutor的文档。

事件处理

等待用户单击UI中的内容时循环播放是错误的。而是使用UI工具包的事件处理功能。在Swing中,例如:

// A "SingleThreadExecutor" has one work thread and an unlimited queue
ExecutorService executor = Executors.newSingleThreadExecutor();

Runnable eatPizza = () -> { System.out.println("Eating a delicious pizza"); };
Runnable cleanUp = () -> { System.out.println("Cleaning up the house"); };

// we submit tasks which will be executed on the work thread
executor.execute(eatPizza);
executor.execute(cleanUp);
// we continue immediately without needing to wait for the tasks to finish


因为事件处理程序在事件分发线程上运行,所以在事件处理程序中进行长时间的工作会阻止与UI的其他交互,直到工作完成。慢速操作可以在新线程上启动,也可以使用上述一种技术(wait / notify,ExecutorServiceExecutors)调度到等待的线程。您还可以使用为此专门设计的BlockingQueue并自动提供后台工作线程:

JLabel label = new JLabel();
JButton button = new JButton("Click me");
button.addActionListener((ActionEvent e) -> {
    // This event listener is run when the button is clicked.
    // We don't need to loop while waiting.
    label.setText("Button was clicked");
});


计时器

执行定期操作,您可以使用Executor。它比编写自己的定时循环更容易使用,并且更容易启动和停止。该演示每秒打印一次当前时间:

JLabel label = new JLabel();
JButton button = new JButton("Calculate answer");

// Add a click listener for the button
button.addActionListener((ActionEvent e) -> {

    // Defines MyWorker as a SwingWorker whose result type is String:
    class MyWorker extends SwingWorker<String,Void> {
        @Override
        public String doInBackground() throws Exception {
            // This method is called on a background thread.
            // You can do long work here without blocking the UI.
            // This is just an example:
            Thread.sleep(5000);
            return "Answer is 42";
        }

        @Override
        protected void done() {
            // This method is called on the Swing thread once the work is done
            String result;
            try {
                result = get();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            label.setText(result); // will display "Answer is 42"
        }
    }

    // Start the worker
    new MyWorker().execute();
});


每个SwingWorker都有自己的后台线程,用于执行其计划的java.util.Timer。自然,线程在任务之间休眠,因此它不会占用CPU。

在Swing代码中,还有一个java.util.Timer,它与之类似,但是它在Swing线程上执行侦听器,因此您可以与Swing组件安全地交互,而无需手动切换线程:

Timer timer = new Timer();
TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println(System.currentTimeMillis());
    }
};
timer.scheduleAtFixedRate(task, 0, 1000);


其他方式

如果要编写多线程代码,那是值得的探索这些包中的类以查看可用的内容:


java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent。锁

,另请参阅Java教程的“并发”部分。多线程很复杂,但是有很多可用的帮助!

评论


很专业的回答,读完这个没误解就留在我心中,谢谢

–胡默云·艾哈迈德(Humoyun Ahmad)
15年8月30日在12:39

很棒的答案。我使用Java线程已经有一段时间了,但仍然在这里学到了一些东西(wait()释放了同步锁!)。

–brimborium
15年10月15日在20:05

谢谢Boann!很好的答案,就像是带有示例的全文!是的,也喜欢“ wait()释放同步锁”

– Kirir Ivanou
16-10-14在17:28



java public class ThreadTest {private static boolean flag = false;私有静态类Reader扩展了线程{@Override public void run(){while(flag == false){} System.out.println(flag); }} public static void main(String [] args){new Reader()。start();标志= true; @Boann,这段代码没有在循环外提升pizzaArrived == false测试,并且循环可以看到主线程更改了标志,为什么?

– Gaussclb
19/12/21在18:30



@gaussclb如果您要反编译一个类文件,请更正。 Java编译器几乎没有优化。吊装由JVM完成。您需要反汇编本机代码。尝试:wiki.openjdk.java.net/display/HotSpot/PrintAssembly

– Boann
19/12/22在13:48