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

69

根据:

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

在新的内存模型中,当线程A向易失变量V写入,并且线程B从V读取时,任何在V写入时对A可见的变量值现在都保证对B可见

许多互联网上的资料都指出,以下代码永远不应该打印"error":

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;
    }
}

a 为 1 时,所有线程的 b 应该为 1。

然而,有时会打印出 "error"。这是怎么可能的?


1
@OliCharlesworth 我认为他在问为什么在对 a 进行 volatile 写入/读取后,b 的各种缓存值没有同步到 b=1 - yshavit
1
您是否真正运行过这段代码,并在 Java 1.5+ 上看到了“error”打印出来? - assylias
4
根据Java内存模型,无论b是否是volatile,都没有关系,因为对它的写入后面跟随着对一个volatile变量的写入,在另一个线程中对它的读取在读取相同的volatile变量之前。 - Marko Topolnik
17
这个主题现在正在Java并发兴趣邮件列表中讨论:http://cs.oswego.edu/pipermail/concurrency-interest/2012-May/009440.html - yshavit
4
从concurrency-interest列表中快速更新,最新的Java7似乎已经解决了这个问题:http://download.java.net/jdk7u6/changes/jdk7u6-b14.html(请查看hotspot部分的最后一项。bug ID链接到一个包含您用例的错误报告)。 - yshavit
显示剩余10条评论
4个回答

34

更新:

对于任何感兴趣的人,这个错误已经得到解决并修复了Java 7u6版本的b14构建。您可以在这里查看错误报告/修复。

原始答案

在考虑内存可见性/顺序时,您需要考虑其发生之前的关系。对于b != 0的重要前提是a == 1。如果a != 1,那么b可以是0或1。

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

在Java 5之后,在OP的例子中,一旦while(a == 0)跳出循环,b的值就保证是1。
编辑: 我运行了很多次模拟,但没有看到你的输出。
你在哪个操作系统、Java版本和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条件中使用(对该值的引用不是tt)。所以如果b == 0为真,这可能意味着对tt的本地存储为0(此时将1分配给本地tt是一场竞赛)。这似乎只对32位Java 1.6和7与客户端设置为真。
我比较了两个输出汇编代码,立即发现了差异。(请记住这些只是片段)。 这打印了"error"
 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

在这个例子中,第一个条目是来自一个打印了"error"的运行,而第二个条目是来自一个没有打印"error"的运行。
看起来,在测试变量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
它是否将 b == 0 作为 JIT 优化移除了? - John Vint
2
我使用了-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintAssembly,从Eclipse调试运行(也可以在正常运行中获得),使用JDK6u30 32位(在64位机器上)。 - Walter Laan
@WalterLaan 这是Java 6 + 32位版本。明白了,这一定是个bug。 - John Vint
2
我敢打赌这是一个OSR错误,这并不是第一次发生OSR错误了(包括TieredCompilation w/ c1->c2和JVM崩溃)。 - bestsss
@JohnVint: 将b加载到tt中将在本地存储该字段,然后在if条件语句中使用 - 它不会“将静态值存储到本地变量中”,它们是整数,本地变量只会具有相同的值。其次,...然后在if条件语句中使用(引用该值而不是tt)... - 同样,它们是原始整数,Java是按值调用的。第三,由于它们是原始整数,int tt = b; //会使jvm缓存b的值吗?你能澄清一下吗? - Farhan stands with Palestine
显示剩余14条评论

12
根据以下的JCiP摘录,我认为你的例子不应该打印“error”:
当线程A写入一个volatile变量并且随后线程B读取同一变量时,所有在A写入volatile变量之前对A可见的变量的值,在B读取volatile变量后将对B可见。

在OS X上,HotSpot(TM) 64位服务器VM(构建20.6-b01-415,混合模式)无法重现。 - Marko Topolnik
2
@JohnVint 太好了。使用“-d32”可以可靠地重现问题。 - Marko Topolnik
1
@MarkoTopolnik,从汇编代码来看(虽然我可能错了),当它失败时,它引用了“tt”的本地存储。其中'b'==0是真的。 - John Vint
1
@JohnVint 我正在查看[concurrency-interest]中的线程,只有32位-client会引起问题。我已经在我的机器上验证过了,使用-server时我没有看到这种影响。 - Marko Topolnik
没有人会编写一个真正的程序,期望'b'具有最新的值,无论规范“保证”什么。 - Monstieur
显示剩余7条评论

2

-3

在我看来,这个问题是由于缺乏同步导致的:

注意:如果b=1发生在a=1之前,而a是易失性的,而b不是,则根据引用的逻辑,实际上只有在a=1完成后,b=1才会为所有线程更新。

在你的代码中发生的情况是,b=1首先仅为主进程更新,然后只有在易失性赋值完成后,所有线程的b才会更新。我认为,可能易失性分配不像原子操作一样工作(需要指向远处,并以某种方式更新其余引用以像易失性一样运行),所以这就是为什么一个线程读取b=0而不是b=1的原因。

考虑对代码进行以下更改,以证明我的说法:

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;
        }  
    }
}

4
很抱歉,您的观点是不正确的。这里有具体的要求,请看http://g.oswego.edu/dl/jmm/cookbook.html。在`Can Reorder`表格中你会发现Java内存模型(JMM)保证的同步。其中至关重要的两个部分是:1.正常存储操作不能与后续的volatile存储操作重排;2.volatile读操作不能与后续的普通读操作重排。这两个规则均适用于本例。 - John Vint

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接