为啥即使我修改了 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还能用吗?忙等待循环也很糟糕,我不建议您使用它们。考虑改用waitnotify @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 的解决方案是正确的:当线程访问共享变量时,无论是通过volatilesynchronized 等,您绝对必须进行某种同步。

我还要补充一点,您的 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 实时重新加载也会说“没有改变”。为啥?

为啥即使数据库中不存在 id 和/或用户名我也会成功

为啥即使我使用 sparse_categorical_crossentrpy,我也会收到“收到超出 [0, 1) 有效范围的标签值 6”?

为啥即使存在列,我也会有 ORA-00904?