即使我修改了锁变量,为什么我会得到一个无限循环?

public class GuardedBlock { private boolean guard = false; private static void threadMessage(String message) { System.out.println(Thread.currentThread().getName() + ": " + message); } public static void main(String[] args) { GuardedBlock guardedBlock = new GuardedBlock(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); guardedBlock.guard = true; threadMessage("Set guard=true"); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { threadMessage("Start waiting"); while (!guardedBlock.guard) { //threadMessage("Still waiting..."); } threadMessage("Finally!"); } }); thread1.start(); thread2.start(); } } 

我通过java essentials教程学习并发。 得到防护块并试图测试它。 有一点我无法理解。

虽然循环是无限的,但如果取消注释threadMessage行,一切正常。 为什么?

简短的回答

你忘了将guard声明为volatile布尔值。


如果您将字段的声明省略为volatile ,则不会告诉JVM多个线程可以看到此字段,在您的示例中就是这种情况。

在这种情况下, guard的值只能读取一次并导致无限循环。 它将被优化为这样的东西(没有打印):

 if(!guard) { while(true) { } } 

现在为什么System.out.println改变这种行为? 因为writes是同步的,这会强制线程不缓存读取。

这里是System.out.println使用的PrintStreamprintln方法之一的代码粘贴:

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

write方法:

 private void write(String s) { try { synchronized (this) { ensureOpen(); textOut.write(s); textOut.flushBuffer(); charOut.flushBuffer(); if (autoFlush && (s.indexOf('\n') >= 0)) out.flush(); } } catch (InterruptedIOException x) { Thread.currentThread().interrupt(); } catch (IOException x) { trouble = true; } } 

注意同步。

Jean-Francois的解决方案是正确的:当线程访问共享变量时,绝对必须有某种同步,无论是通过volatilesynchronized等。

我还要补充一点,你的while循环相当于所谓的忙等待 – 也就是说,在并发设置中反复测试一个条件。 在此代码中繁忙等待的紧密循环可能会占用CPU。 至少它对系统资源的影响是不可预测的。

您可能希望探索条件变量方法来处理受单个共享条件影响的多个线程。 Java在java.util.concurrent库中有许多更高级别的工具,但是了解较旧的低级API方法是很好的,特别是因为您已经直接使用Thread实例。

每个Object都有wait()notifyAll()方法。 Object表示条件,或至少表示与之关联的监视器。 在一个测试条件的while循环中调用wait()方法,并阻塞调用线程,直到其他一些线程调用notifyAll() 。 然后所有等待的线程都会被唤醒,他们都将争夺锁定并再次测试这个条件的机会。 如果条件在该点保持为真,那么所有线程都将继续。

以下是使用此方法的代码:

 public class GuardedBlock { private boolean guard = false; private static void threadMessage(String message) { System.out.println(Thread.currentThread().getName() + ": " + message); } public static void main(String[] args) throws Exception { GuardedBlock guardedBlock = new GuardedBlock(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); synchronized (guardedBlock) { guardedBlock.guard = true; guardedBlock.notifyAll(); } threadMessage("Set guard=true"); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { threadMessage("Start waiting"); while (!guardedBlock.guard) { synchronized (guardedBlock) { try { guardedBlock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } threadMessage("Finally!"); } }); thread1.start(); thread2.start(); thread2.join(); System.out.println("Done"); } } 

请注意以下事项:

  • 无论何时读取或写入条件(通过synchronized关键字),都必须进行锁定。
  • while循环中测试guard条件,但是在wait()调用期间该循环阻塞。 它仍然是一个while循环的唯一原因是处理有许multithreading且条件变化很多次的情况。 然后,当线程被唤醒时,应该重新测试该条件,以防另一个线程在唤醒和重新获取锁之间的微小间隙中改变条件。
  • guard条件设置为true时,将通知等待线程(通过notifyAll()调用。)
  • 最后的thread2实例上的程序块,这样我们就不会在所有线程完成之前退出主线程(通过join()调用。)

如果你看一下Object API,你会发现还有一个notify()方法。 始终使用notifyAll()更简单,但如果您想了解这两种方法之间的区别, 请参阅此SOpost 。

你的while循环无限的原因是条件!guardedBlock.guard始终为true。 这意味着guardedBlock.guard = true; 没有为线程2设置线程1中的设置,这是因为您没有将变量保护用作volatile

让我复制从维基百科本身使用java中的volatile的需要:

在所有Java版本中,对volatile变量的读写都有一个全局排序。 这意味着访问volatile字段的每个线程将在继续之前读取其当前值 ,而不是(可能)使用缓存值。

希望有所帮助。