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行,一切正常。为什么呢?
答案 0 :(得分:15)
简短回答
您忘记将guard
声明为易变的布尔值。
如果你将字段的声明省略为volatile
,那么你并没有告诉JVM多个线程可以看到这个字段,例如你的情况。
在这种情况下,guard
的值只能读取一次并导致无限循环。它将被优化为这样的东西(没有打印):
if(!guard)
{
while(true)
{
}
}
为什么System.out.println
会改变这种行为?因为writes
是同步的,它会强制线程不缓存读取。
此处粘贴了println
使用的PrintStream
System.out.println
方法之一的代码:
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;
}
}
注意同步。
答案 1 :(得分:3)
Jean-Francois的解决方案是正确的:当线程访问共享变量时,绝对必须有某种同步,无论是通过volatile
,synchronized
等等。
我还要补充一点,你的while
循环相当于所谓的busy waiting - 也就是说,在并发设置中反复测试一个条件。在此代码中繁忙等待的紧密循环可能会占用CPU。至少它对系统资源的影响是不可预测的。
您可能希望探索condition variable方法来处理受单个共享条件影响的多个线程。 Java在java.util.concurrent
库中有许多更高级别的工具,但是了解较旧的低级API方法是很好的,特别是因为您已经直接使用Thread
个实例。
每个Object
都有wait()
和notifyAll()
种方法。 Object
代表条件,或者至少代表与之关联的监视器。在wait()
循环中调用while
方法来测试条件,并阻塞调用线程,直到某个其他线程调用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
关键字。)guard
条件在while
循环内进行测试,但该循环在wait()
调用期间阻塞。它仍然是while
循环的唯一原因是处理有许多线程且条件发生多次变化的情况。然后,当线程被唤醒时,应该重新测试该条件,以防另一个线程在唤醒和重新获取锁之间的微小间隙中改变条件。guard
条件设置为true
时(通过notifyAll()
来电,会通知等待线程。)thread2
实例上的程序块
非常结束,这样我们就不会在所有线程之前退出主线程
完成(通过join()
电话。)如果查看Object
API,您会看到还有notify()
方法。始终使用notifyAll()
更简单,但如果您想了解这两种方法之间的区别,see this SO post。
答案 2 :(得分:1)
你的while循环无限的原因是条件!guardedBlock.guard
始终为真。这意味着线程1中设置的guardedBlock.guard = true;
没有为线程2设置,这是因为您没有将变量保护用作volatile
。
让我复制从维基百科本身使用java中的volatile的需要:
在所有Java版本中,对volatile变量的读写都有一个全局排序。这意味着访问volatile字段的每个线程将在继续之前读取其当前值,而不是(可能)使用缓存值。
希望有所帮助。