69

根据:

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

在新的内存模型下,当线程 A 写入 volatile 变量 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时,所有线程都应a为 1。

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

4

4 回答 4

34

更新:

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

原始答案

在考虑内存可见性/顺序时,您需要考虑其发生之前的关系。的重要前提条件b != 0是 for 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,或者更确切地说是 staticgetb似乎有很大的影响(为了证明这删除了int tt = b;​​并且它应该总是有效的。

看起来binto的加载tt将在本地存储该字段,然后在 if coniditonal 中使用该字段(对该值的引用 not tt)。因此,如果b == 0为真,则可能意味着本地存储为tt0(此时将 1 分配给 local tt)。这似乎只适用于带有客户端集的 32 位 Java 1.6 和 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 时,包括在内时似乎有 100% 的错误率,int tt = b而在排除时似乎有 0% 的机会。

于 2012-05-16T15:07:26.080 回答
12

根据下面 JCiP 的摘录,我认为您的示例永远不应该打印“错误”:

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

于 2012-05-16T14:59:04.523 回答
2

您可能想查看有关此问题的并发兴趣邮件列表上的讨论线程:http: //cs.oswego.edu/pipermail/concurrency-interest/2012-May/009440.html

使用客户端 JVM (-client) 似乎更容易重现该问题。

于 2012-05-17T03:43:32.560 回答
-2

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

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

您的代码中发生的事情是 b=1 仅针对主进程首先更新,然后仅在 volatile 分配完成时,所有线程 b 才更新。我认为可能 volatile 的分配不能作为原子操作工作(需要指向很远,并以某种方式更新其余的引用以像 volatile 一样工作)所以这就是我猜测为什么一个线程读取 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;
        }  
    }
}
于 2012-05-16T18:25:57.197 回答