为什么java 5+中的volatile不能确保来自另一个线程的可见性?

时间:2012-05-16 14:26:48

标签: java multithreading concurrency volatile compiler-bug

根据:

http://www.ibm.com/developerworks/library/j-jtp03304/

  

在新内存模型下,当线程A写入易失性变量V,线程B从V读取时,保证在写入V时A可见的任何变量值对B < / p>

互联网上的许多地方声明以下代码永远不应该打印“错误”:

public class Test {
    volatile static private int a;
    static private int b;

    public static void main(String [] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread() {

                @Override
                public void run() {
                    int tt = b; // makes the jvm cache the value of b

                    while (a==0) {

                    }

                    if (b == 0) {
                        System.out.println("error");
                    }
                }

            }.start();
        }

        b = 1;
        a = 1;
    }
}
b为1时,

a 应该为所有主题。

然而我有时会打印“错误”。这怎么可能?

4 个答案:

答案 0 :(得分:34)

<强>更新

对于任何感兴趣的人来说,这个bug已经针对Java 7u6 build b14进行了解决和修复。你可以在这里看到bug报告/修复

原始答案

在考虑内存可见性/顺序时,您需要考虑它之前发生的关系。 b != 0的重要前提条件是a == 1。如果a != 1则b可以是0或1.

一旦线程看到a == 1,那么该线程就会保证看到b == 1

在OP示例中发布Java 5,一旦while(a == 0)突破b保证为1

修改

我多次运行模拟并没有看到你的输出。

什么操作系统,Java版本&amp;你正在测试CPU吗?

我在Windows 7,Java 1.6_24(尝试使用_31)

编辑2:

对OP和Walter Laan的称赞 - 对我来说,只有当我从64位Java切换到32位Java时才会发生(但可能不会被排除在外)64位Windows 7。

编辑3:

tt的分配,或者更确切地说b的静态分配似乎产生了重大影响(为了证明这一点已删除int tt = b;,它应该始终有效。

b tt的加载似乎会在本地存储该字段,然后将在if coniditonal中使用该字段(对该值的引用不是tt)。因此,如果b == 0为真,则可能意味着tt的本地存储为0(此时它是一个将1分配给本地tt的竞赛)。这似乎只适用于32位Java 1.6&amp; 7与客户端设置。

我对两个输出组件进行了比较,现在的区别就在于此。 (请记住这些是片段)。

此印刷的“错误”

 0x021dd753: test   %eax,0x180100      ;   {poll}
  0x021dd759: cmp    $0x0,%ecx
  0x021dd75c: je     0x021dd748         ;*ifeq
                                        ; - Test$1::run@7 (line 13)
  0x021dd75e: cmp    $0x0,%edx
  0x021dd761: jne    0x021dd788         ;*ifne
                                        ; - Test$1::run@13 (line 17)
  0x021dd767: nop    
  0x021dd768: jmp    0x021dd7b8         ;   {no_reloc}
  0x021dd76d: xchg   %ax,%ax
  0x021dd770: jmp    0x021dd7d2         ; implicit exception: dispatches to 0x021dd7c2
  0x021dd775: nop                       ;*getstatic out
                                        ; - Test$1::run@16 (line 18)
  0x021dd776: cmp    (%ecx),%eax        ; implicit exception: dispatches to 0x021dd7dc
  0x021dd778: mov    $0x39239500,%edx   ;*invokevirtual println

并且

这不会打印“错误”

0x0226d763: test   %eax,0x180100      ;   {poll}
  0x0226d769: cmp    $0x0,%edx
  0x0226d76c: je     0x0226d758         ;*ifeq
                                        ; - Test$1::run@7 (line 13)
  0x0226d76e: mov    $0x341b77f8,%edx   ;   {oop('Test')}
  0x0226d773: mov    0x154(%edx),%edx   ;*getstatic b
                                        ; - Test::access$0@0 (line 3)
                                        ; - Test$1::run@10 (line 17)
  0x0226d779: cmp    $0x0,%edx
  0x0226d77c: jne    0x0226d7a8         ;*ifne
                                        ; - Test$1::run@13 (line 17)
  0x0226d782: nopw   0x0(%eax,%eax,1)
  0x0226d788: jmp    0x0226d7ed         ;   {no_reloc}
  0x0226d78d: xchg   %ax,%ax
  0x0226d790: jmp    0x0226d807         ; implicit exception: dispatches to 0x0226d7f7
  0x0226d795: nop                       ;*getstatic out
                                        ; - Test$1::run@16 (line 18)
  0x0226d796: cmp    (%ecx),%eax        ; implicit exception: dispatches to 0x0226d811
  0x0226d798: mov    $0x39239500,%edx   ;*invokevirtual println

在这个例子中,第一个条目来自一个打印“错误”的运行,而第二个条目来自一个没有的错误。

工作运行似乎在测试之前正确加载并分配b等于0。

  0x0226d76e: mov    $0x341b77f8,%edx   ;   {oop('Test')}
  0x0226d773: mov    0x154(%edx),%edx   ;*getstatic b
                                        ; - Test::access$0@0 (line 3)
                                        ; - Test$1::run@10 (line 17)
  0x0226d779: cmp    $0x0,%edx
  0x0226d77c: jne    0x0226d7a8         ;*ifne
                                        ; - Test$1::run@13 (line 17)

打印“错误”的运行加载了%edx

的缓存版本
  0x021dd75e: cmp    $0x0,%edx
  0x021dd761: jne    0x021dd788         ;*ifne
                                        ; - Test$1::run@13 (line 17)

对于那些对汇编程序有更多经验的人,请权衡:)

编辑4

应该是我的最后一次编辑,因为并发开发者可以得到它,我做了测试有没有 int tt = b;再分配一些。我发现当我将最大值从100增加到1000时,如果包含int tt = b,则错误率似乎为100%,排除时则为0%。

答案 1 :(得分:12)

基于以下JCiP的摘录,我认为你的例子永远不应该打印“错误”:

  

volatile变量的可见性效果超出了volatile变量本身的值。当一个线程 A 写入一个volatile变量,随后线程 B 读取同一个变量时,所有变量的值对于可见>在读取volatile变量后,写入volatile变量之前的 B 可见。

答案 2 :(得分:2)

您可能希望在此问题上查看并发兴趣邮件列表上的讨论主题:http://cs.oswego.edu/pipermail/concurrency-interest/2012-May/009440.html

似乎问题更容易通过客户端JVM(-client)重现。

答案 3 :(得分:-2)

在我看来,由于缺乏同步而引发问题:

注意:如果在a = 1之前b = 1 heppens,并且a是易失性而b不是,则b = 1实际上仅在a = 1完成后才为所有线程更新(根据quate的逻辑)。

代码中的内容是b = 1首先仅针对主进程更新,然后仅在volatile分配完成时才更新所有线程b。我认为也许volatile的赋值不能用作原子操作(需要指向远处,并以某种方式更新其余的引用以像挥发物一样)所以这就是我的猜测为什么一个线程读取b = 0而不是b = 1。 / p>

考虑对代码的这一更改,它显示了我的主张:

public class Test {
    volatile static private int a;
    static private int b;
    private static Object lock = new Object();


    public static void main(String [] args) throws Exception {
        for (int i = 0; i < 100; i++) {
            new Thread() {

                @Override
                public void run() {
                    int tt = b; // makes the jvm cache the value of b

                    while (true) {
                        synchronized (lock ) {
                            if (a!=0) break;
                         }
                    }

                    if (b == 0) {
                        System.out.println("error");
                    }
                }

            }.start();
        }
        b = 1;
        synchronized (lock ) {
        a = 1;
        }  
    }
}