为什么在java 5+中不稳定不能确保从另一个线程的可见性?

根据:

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

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

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

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 时,对于所有线程,为 1。a

但是,我有时会打印“错误”。这怎么可能?


答案 1

更新:

对于任何感兴趣的人,这个错误已经在Java 7u6 build b14中得到解决和修复。您可以在此处查看错误报告/修复

原始答案

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

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

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

编辑:

我多次运行模拟,但看不到您的输出。

您正在测试什么操作系统,Java版本和CPU?

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

编辑 2:

向OP和Walter Laan致敬 - 对我来说,只有当我从64位Java切换到32位Java时,才会发生这种情况,打开(但可能不会排除)64位Windows 7。

编辑3:

赋值给 ,或者更确切地说是 staticget 的赋值似乎有重大影响(为了证明这一点,删除 它应该总是有效。ttbint tt = b;

看起来 into 的负载将在本地存储字段,然后将其用于 if 分生子(对该值的引用不是 )。因此,如果为真,则可能意味着本地存储为0(此时,将1分配给本地是一场竞赛)。这似乎只适用于具有客户端集的 32 位 Java 1.6 和 7。bttttb == 0tttt

我比较了两个输出组件,直接的区别就在这里。(请记住,这些是片段)。

此打印的“错误”

 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

在此示例中,第一个条目来自打印“错误”的运行,而第二个条目来自未打印“错误”的运行。

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

  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

应该是我最后一次编辑,因为并发开发人员已经掌握了它,我确实在有和没有分配的情况下进行了更多的测试。我发现,当我将最大值从100增加到1000时,当包含时似乎有100%的错误率,当它被排除时,有0%的几率。int tt = b;int tt = b


答案 2

根据下面的JCiP摘录,我本来以为你的例子永远不应该打印“error”:

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


推荐