为啥即使我修改了 lock 变量,我也会得到一个无限的 while 循环? [复制]
Posted
技术标签:
【中文标题】为啥即使我修改了 lock 变量,我也会得到一个无限的 while 循环? [复制]【英文标题】:Why do I get an infinite while loop even if I modify the lock variable? [duplicate]为什么即使我修改了 lock 变量,我也会得到一个无限的 while 循环? [复制] 【发布时间】:2015-09-07 00:49:18 【问题描述】: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 将不会对其他线程可见。
当您注释掉该行时,您的 while 循环中就没有任何东西了,Java 足够聪明,可以忽略整个 while 循环。所以,它和没有 while 循环一样好。
一切正常是什么意思。它打印还在等待吗?那么最后?
把guard
改成volatile guard
还能用吗?忙等待循环也很糟糕,我不建议您使用它们。考虑改用wait
和notify
。
@hagrawal:这不可能。如果是这样,OP 将不会遇到无限循环。相反,正如 akhil_mittal 所说,guard
不是volatile
。所以它的值不能保证在线程之间同步。添加threadMessage
行必须触发某种内存屏障,这恰好有助于同步线程对guard
值的视图。但这是非常不可靠的行为。
【参考方案1】:
简答
您忘记将 guard
声明为 volatile 布尔值。
如果您省略将字段声明为 volatile
,您并没有告诉 JVM 该字段可以被多个线程看到,在您的示例中就是这种情况。
在这种情况下,guard
的值只会被读取一次,并会导致无限循环。它将被优化为这样的东西(没有打印):
if(!guard)
while(true)
现在为什么System.out.println
改变这种行为?因为writes
是同步的,这会强制线程不缓存读取。
这里是PrintStream
使用的println
方法之一的代码粘贴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;
注意同步。
【讨论】:
【参考方案2】: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。
【讨论】:
【参考方案3】:您的 while 循环无限的原因是条件 !guardedBlock.guard
始终为真。这意味着在线程 1 中设置的 guardedBlock.guard = true;
没有为线程 2 设置,这是因为您没有将变量保护用作 volatile
。
让我从***本身复制在 java 中使用 volatile 的需要:
在所有版本的 Java 中,对 volatile 变量的读取和写入都有一个全局顺序。这意味着每个访问 volatile 字段的线程都会在继续之前读取其当前值,而不是(可能)使用缓存值。
希望对您有所帮助。
【讨论】:
以上是关于为啥即使我修改了 lock 变量,我也会得到一个无限的 while 循环? [复制]的主要内容,如果未能解决你的问题,请参考以下文章
为啥即使我有一个 catch() 函数,我也会收到 UnhandledPromiseRejectionWarning?
为啥即使实现了 Iterable,我也会收到 foreach 编译器错误?
即使我修改了文件,Webpack 实时重新加载也会说“没有改变”。为啥?
为啥即使我使用 sparse_categorical_crossentrpy,我也会收到“收到超出 [0, 1) 有效范围的标签值 6”?