了解Java volatile字段的协调性。阅读&跨线程写

时间:2016-09-04 02:05:56

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

我有以下代码:

 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...

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

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

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

或者这种行为是预期的,如果是这样,为什么?感谢。

修改:正如评论中所要求的那样,我使用预期的输出更新帖子,我有时会看到但不是所有的时间:

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

3 个答案:

答案 0 :(得分:2)

我没有看到问题。这里的锁是没用的。 volatile也意味着变量在其自身内同步。发生了什么。每当有多个线程时,每个线程都会自行运行而不需要关注其他线程。所以在这种情况下我们有两个线程:main和thread-0。主要运行并且到达打印变量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);
//=================

答案 1 :(得分:1)

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

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

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

  1. 最终看到这个新值。
  2. 未读取之前使用&#34; old&#34;写入先前写入volatile字段的任何值。值。
  3. 另请注意,调用System.out.println会在System.out对象上隐式同步(请查看PrintWriter代码)。考虑到您在单个监视器上同步两个线程的事实也解释了观察到的输出。我假设在您的字段设置线程锁定System.out监视器时创建了字符串。在这种情况下,线程首先创建要写入的字符串,然后等待另一个线程释放此监视器,这就是为什么输出带有&#34; old&#34;内容是你经常观察到的东西。

    我的意思是声明

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

    不是原子的。分为两步,该陈述相当于:

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

    鉴于这种非原子性,事件链(命名你的线程 A B )就像:

    /*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

答案 2 :(得分:1)

如果你只看一下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语句需要在同一个锁上的另一个同步块中。