【问题标题】:Understanding of coordination for Java volatile fields' reads & writes across threads理解Java volatile字段跨线程读写的协调
【发布时间】:2016-09-04 02:05:56
【问题描述】:

我有以下代码:

 private volatile boolean run = true;

 private Object lock =new Object();

…………

Thread newThread = new Thread(new Runnable() {

    @Override
        public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {

            e.printStackTrace();
        }
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName()
                        + " run:" + run);

                System.out.println(Thread.currentThread().getName()
                        + " setting run to false");

                run = false;

                System.out.println(Thread.currentThread().getName()
                        + " run:" + run);
            }
        }});

newThread.start();

while(true) {//no synchronization, so no coordination guarantee
    System.out.println(Thread.currentThread().getName() + "* run: "+run);

    if(run == false) {
    System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting...");
    System.exit(0);
    }
}




which generates the following output:



main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
Thread-0 setting run to false
Thread-0 run:false
main* run: true    <- what causes this???
main** run: false
Exiting...

我试图理解为什么在主线程中出现 ma​​in* run: true 的异常,因为 run 是一个易失性字段,并且根据 Java 内存模型规范,易失性写入Thread-0 中的 mainthread 应该立即可见。我知道Thread-0 中的同步在这里是无关紧要的,但我对 volatile 的这种行为感到困惑。我在这里想念什么?

另一个更奇怪的运行产生了这个:

main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main** run: false
Exiting...
Thread-0 run:false

或者这种行为是意料之中的,如果是,为什么?谢谢。

编辑:正如 cmets 中所问的,我正在使用我有时但并非总是看到的预期输出更新帖子:

main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
main* run: true
Thread-0 setting run to false
main* run: true
main* run: true
main* run: true
Thread-0 run:false
main** run: false
Exiting...

也就是说,我不想看到:

main* run: true 

出现在之后

Thread-0 run:false

main** run: false
Exiting...

出现在之前

Thread-0 run:false

【问题讨论】:

  • 请制作一个mvce。想要运行您发布的代码的人不应该必须从不完整的 sn-ps 重构它。
  • @NathanHughes:什么是 mvce?
  • 此场景中还有另一个共享资源:System.out
  • mvce = 最有价值的代码示例。
  • @MikeSamuel:你介意解释一下吗?

标签: java multithreading java.util.concurrent java-memory-model


【解决方案1】:

我没有看到问题。这里的锁没用。 volatile 也意味着变量在自身内部是同步的。这里发生了什么。每当有多个线程时,每个线程都会自行运行,而无需关心其他线程。所以在这种情况下,我们有两个线程:main 和 thread-0。 Main 自行运行并到达打印变量run 的位置,因此它会打印它。另一个线程睡了一会儿(这应该无关紧要,也不应该是让其他线程先工作的一种方式),然后将变量run 更改为false。主线程读取新值并存在

按照时间顺序你就会明白

Thread newThread = new Thread(new Runnable() {

@Override
    public void run() {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {

        e.printStackTrace();
    }
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName()
                    + " run:" + run);

            System.out.println(Thread.currentThread().getName()
                    + " setting run to false");

            run = false; //<---- time_4

            System.out.println(Thread.currentThread().getName()
                    + " run:" + run); //<---- time_5
        }
    }});

newThread.start();

while(true) { //<---- time_2
    System.out.println(Thread.currentThread().getName() + "* run: "+run); //<--- time_3 getting the value of run variable. //<---- time_6 printing

    if(run == false) { //<---- time_1 (run == true) // <---- 2nd iteration time_7 (run == false)
    System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting..."); //<---- time_8
    System.exit(0);
    }
}

无论如何,这里是如何修复你的代码以获得预期的输出(注意:volatile 在这里没有做任何事情):

synchronized (lock) {
                if(run == false) {
                    System.out.println(Thread.currentThread().getName() + "** run: "+run+"\nExiting...");
                    System.exit(0);
                }
            }

这就是 volatile 对变量 run 的基本作用:

// run = false; //becomes ========
synchronized(someLock) {
    run = flase;
}
// =======================


//System.out.println(run); //becomes =========   
synchronized(someLock) {
    boolean tmpBoolean = run;
}
System.out.println(tmpBoolean);
//=================

【讨论】:

  • 好吧,我希望正确的行为类似于main* run: true main* run: true main* run: true main* run: true main* run: true Thread-0 setting run to false main* run: true main* run: true main* run: true Thread-0 run:false main** run: false Exiting... ,有时会发生这种情况。那么为什么会出现上述偏差呢?
  • 您能否以预期的行为编辑帖子。很难在评论中阅读谢谢
  • 是的,我了解您描述的流程。然而,让我感到困惑的是,即使run 被指定为易失性,main 线程在Tread-0(参见第一个 sn-p)和第二个,事情的流程(根据 System.out.println)似乎完全不正常:main 甚至在runThread-0 中更新之前就退出了
  • 我在帖子的文本中添加了预期的行为。谢谢
  • 不确定我是否关注。您输入的时间戳似乎是在 main 线程中重新定义 program order - 它不会违反根据 Java 内存模型规范定义为 volatilerun 的合同吗?
【解决方案2】:

如果只看volatile变量的读写,那么它们必须按顺序出现:

1 - main: read run (run is true)
2 - Thread-0: write run (run is false)
3 - main: read run (run is false)

但控制台输出是单独的操作,不需要在读取后立即发生。对 println 参数的评估和调用方法不是原子的。所以我们有更多类似的东西:

1 - main: read run (run is true)
2 - main: println("Run: true")

3 - Thread-0: write run (run is false)
4 - Thread-0: println("Run: false")

5 - main: read run (run is false)
6 - main: println("Run: false")

这允许在第一个排序之后进行排序,例如:

1 - main: read run (run is true)

3 - Thread-0: write run (run is false)
4 - Thread-0: println("Run: false")

2 - main: println("Run: true")

5 - main: read run (run is false)
6 - main: println("Run: false")

基于PrintWriter中的源代码,行:

System.out.println(Thread.currentThread().getName() + " run:" + run);

可以内联如下:

String x = Thread.currentThread().getName() + " run:" + run;
synchronized(System.out.lock) {
    System.out.print(x);
    System.out.println();
}

所以println 中存在同步,但不包括对run 的读取。这意味着run 的值可以在读取和输出之间发生变化,从而导致输出run 的旧值。

要获得您期望的输出,同步块需要同时包含runprintln 语句的设置。并且在另一个线程上读取runprintln 语句需要在同一个锁上的另一个同步块中。

【讨论】:

  • 您能否扩展:“对 println 参数的评估和调用方法不是原子的。” println 存在于两个线程中,这将导致其在同一监视器对象上的两个线程之间进行内部同步(但它确实如此,我没有源),我假设。或者这是错误的假设?那么为什么显式同步似乎工作正常(对于我所做的许多试验)但 volatile 有时会产生这种奇怪的异常?根据 JMM,volatilesynchronized 表现出这些隐含差异是否正确?
  • @SimeonLeyzerzon 不同之处在于同步块中包含的语句数。显式同步包括run的读取和写入,而隐式同步不包括,允许它在更改后打印旧值。我已经添加了 PrintWriter 的源代码。
  • 是的,这种解释(类似于拉斐尔所说的,尽管用不同的词,看起来像)从重构事件顺序来看是合乎逻辑的。正如您所说,看起来这种粗略的同步发生在PrintStream(System.out 通过 JNI 设置)内部,在其println(String) 内部,整个打印在this 上同步。我假设这个 PrintStream 以及它通过本机代码注入的事实将使它成为跨不同线程(在我的情况下为 main 和 Thread-0)的单例,但也许这个假设是错误的,这也可以解释......
  • ...不匹配,因为打印在不同的显示器上同步?想知道为什么他们选择以这种方式实现PrintStream 的 println 而没有明确记录其同步行为,考虑到这段代码非常简单,这肯定会让人猜到。为什么不让 println() 和一个字符串一起接受一个外部锁对象,这样我们就可以在设置文本和刷新它的整个边界上显式地保护我们的代码?
  • @SimeonLeyzerzon 是的,PrintStream 的实例在不同的线程中是相同的。我认为 PrintStream 同步的目的只是为了防止一行的一半出现在另一行的中间。因此,一行必须在下一行开始之前完成打印。如果您需要额外的同步,您可以随时自行添加。通常,只使用内部锁来控制其他可以同步的内容,防止死锁或不必要的阻塞。
【解决方案3】:

至于你程序的正确性,synchronized 块是完全没有必要的。如果您只从单个线程同步锁,则很有可能JIT compiler eliminates this lock altogether.

真正让您感到困惑的是 JMM 保证 volatile 字段在写入后可以被其他线程看到其更新值。但是,此保证暗示写入volatile 字段的线程会立即传达此值并停止,直到将新值传达给所有其他线程。相反,保证是其他线程最终需要看到更新的值。

这意味着如果线程 A 写入 volatile 字段,则线程 B 保证:

  1. 终于看到了这个新值。
  2. 不读取之前写入具有“旧”值的 volatile 字段的任何值。

另外,请注意调用System.out.println 隐式同步System.out 对象(查看PrintWriter 代码)。考虑到您在单个监视器上同步两个线程这一事实也解释了观察到的输出。我假设该字符串是在 System.out 监视器被您的字段设置线程锁定时创建的。在这种情况下,线程首先创建要写入的字符串,然后等待其他线程释放此监视器,这就是为什么您通常会观察到带有“旧”内容的输出。

我的意思是声明

System.out.println(Thread.currentThread().getName() + "* run: " + run);

不是原子的。分两步分解,语句等价于:

String text = Thread.currentThread().getName() + "* run: " + run;
System.out.println(text);

鉴于这种非原子性,事件链(将线程命名为 AB)如下:

/*A*/ String text = Thread.currentThread().getName() + "* run: " + run;
/*B*/ System.out.println(Thread.currentThread().getName() + " setting run to false");
/*B*/ run = false;
/*B*/ System.out.println(Thread.currentThread().getName() + " run:" + run);
/*A*/ System.out.println(text);
/*A*/ if(run == false) {
/*A*/ System.out.println(Thread.currentThread().getName() + "** run: " + run + "\nExiting...");
/*A*/ System.exit(0);
/*A*/ }

由于lock coarsening 优化,此结果可能也是最常见的结果,其中每个循环都包含您的任何循环体的全部。在这个粗化锁之外唯一要做的事情是创建第一个字符串值,这是您观察到的旧值。

有关 JMM 的更多信息,我once summarized my understanding in a talk。另外,看看cache coherence protocols,它最终决定了可见性。

【讨论】:

  • Rafael,我昨天听了你的演讲,这部分地启发了我深入了解这个输出的行为。我仍然很困惑。你说:“当 A 想要打印到控制台时,它需要重新获取 System.out 的监视器,这会导致缓冲区被刷新,以便 B 中的后续消息打印更新的消息。”然而,这个顺序与这个前提相矛盾:Thread-0 run:falsemain* run: truemain** run: false。如您所见,main 仍然打印true,即使它在打印时被Thrd-0 刷新。打印是原子的吗?
  • 还是main* run: truemain 线程上未刷新的打印缓冲区的痕迹?但是您是在说:“这意味着如果线程 A 写入 volatile 字段,则线程 B 保证: 2. 不读取以前用其先前值写入的任何值。”我没有完全理解。如果您可以扩展它...似乎volatile 在线程间协调方面并不像synchronized 那样严格,即使JMM 似乎没有明确区分它们,这是否公平要说?
  • 抱歉,我对您预期的输出和未预期的输出感到困惑。我更新了我的答案,其他输出也一样合乎逻辑。
  • 是的,Rafael,正如我刚才对@fgb 的评论,你们俩似乎在说类似的话。他们听起来很明智。也希望您对该评论中的问题发表意见。谢谢。