没有打印语句,Loop看不到更改的值

在我的代码中,我有一个等待某个状态从一个不同的线程更改的循环。 另一个线程工作,但我的循环从来没有看到改变的价值。 它永远等待。 但是,当我在循环中放置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()来设置pizzaArrivedvariables。 但循环只适用于我取消注释System.out.println("waiting"); 声明。 这是怎么回事?

允许JVM假定其他线程在循环过程中不会更改pizzaArrivedvariables。 换句话说,它可以在循环外提升pizzaArrived == falsetesting,优化这个:

 while (pizzaArrived == false) {} 

进入这个:

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

这是一个无限循环。

为了确保一个线程所做的更改对其他线程可见,您必须始终在这些线程之间添加一些同步 。 最简单的方法是使共享variablesvolatile

 volatile boolean pizzaArrived = false; 

做一个variablesvolatile保证不同的线程会看到对方的变化的影响。 这样可以防止JVMcachingpizzaArrived的值或在循环之外提升testing。 相反,它必须每次读取实variables的值。

(更正式地说, volatile会在variables访问之间创build一个before-before关系,这意味着一个线程在交付比萨之前所做的所有其他工作对于接收披萨的线程也是可见的,即使这些其他变化不是volatilevariables。)

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

 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; } } 

打印语句的效果

System.out是一个PrintStream对象。 PrintStream的方法是这样同步的:

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

同步过程可防止pizzaArrived在循环中被caching。 严格来说, 两个线程都必须在同一个对象上同步,以保证对variables的更改是可见的。 (例如,在设置pizzaArrived之后调用println并在读取pizzaArrived之前再次调用它将是正确的。)如果只有一个线程在特定对象上同步,则允许JVM忽略它。 实际上,JVM并不足以certificate其他线程在设置pizzaArrived之后不会调用println ,所以它假定它们可能。 因此,如果您调用System.out.println ,则无法在循环中cachingvariables。 这就是为什么循环这样的工作,当他们有一个打印语句,但它不是一个正确的修复。

使用System.out并不是引起这种效果的唯一方法,但是当人们试图debugging为什么它们的循环不起作用时,它是最常见的一种。


更大的问题

while (pizzaArrived == false) {}是一个忙等待循环。 那很糟! 在等待的时候,会占用CPU,从而减慢其他应用程序的运行速度,并增加系统的功耗,温度和风扇速度。 理想情况下,我们希望循环线程在等待时进入睡眠状态,这样就不会占用CPU。

这里有一些方法来做到这一点:

使用等待/通知

一个低级的解决scheme是使用Object的wait / notify方法 :

 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() ,这使线程hibernate。 睡眠时不会使用任何CPU周期。 第二个线程设置variables后,它调用notifyAll()来唤醒等待该对象的所有线程。 这就像比萨饼家伙敲门铃,所以你可以坐下来rest,而不是站在门口笨拙。

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

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"); } } 

注意: BlockingQueueputtake方法可以抛出InterruptedExceptionexception,这是必须处理的检查exception。 在上面的代码中,为了简单起见,重新排除了exception。 您可能更喜欢在方法中捕获exception,然后重试put或take调用以确保成功。 除此之外, BlockingQueue非常容易使用。

在这里不需要其他同步,因为BlockingQueue确保线程在将项目放入队列之前所做的所有操作都是可见的。

执行人

Executor就像现成的执行任务的BlockingQueue 。 例:

 // 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 

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

事件处理

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

 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"); }); 

因为事件处理程序在事件派发线程上运行,所以在事件处理程序中长时间工作会阻止与UI的其他交互,直到工作完成。 慢操作可以在新线程上启动,或者使用上述技术之一(wait / notify, BlockingQueueExecutor )分派给等待的线程。 您也可以使用专为此devise的SwingWorker ,并自动提供后台工作线程:

 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(); }); 

计时器

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

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

每个java.util.Timer都有自己的后台线程,用于执行其计划的TimerTask 。 当然,线程在任务之间hibernate,所以它不占用CPU。

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

 JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Timer timer = new Timer(1000, (ActionEvent e) -> { frame.setTitle(String.valueOf(System.currentTimeMillis())); }); timer.setRepeats(true); timer.start(); frame.setVisible(true); 

其他方法

如果您正在编写multithreading代码,那么值得探索这些包中的类来查看可用的内容:

  • java.util.concurrent中
  • java.util.concurrent.atomic中
  • java.util.concurrent.locks中

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