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


68

根据:

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,所有的线程a是1。

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


1
@OliCharlesworth我想他是在问为什么在易失性的写入/读取之后,各种缓存值b不同步。b=1a
yshavit 2012年

1
使用Java 1.5+,您是否实际运行过该代码并看到“错误”打印?
assylias 2012年

4
@OfekRon根据Java内存模型,它是否b易失性并不重要,因为对其进行写操作之后是对易失性var的写操作,而在另一个线程中,对其的读取之前是对同一变量的读取挥发性变种
Marko Topolnik

17
这线程现在正在讨论在Java并发利益邮件列表:cs.oswego.edu/pipermail/concurrency-interest/2012-May/...
yshavit

4
只需从并发兴趣列表中进行快速更新,就可以在最新的Java7中修复此问题:download.java.net/jdk7u6/changes/jdk7u6-b14.html(请查看“热点”部分中的最后一项。) ID链接到您的用例的错误报告
yshavit 2012年

Answers:


34

更新:

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

原始答案

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

一旦看到a == 1线程,便保证该线程看到b == 1

在OP示例中,在Java 5之后,一旦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;,它应该一直有效。

它出现的负载btt将本地存储的字段,它然后将在如果coniditonal被使用(参考到该值不tt)。因此,如果b == 0为true,则可能意味着to的本地存储为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时int tt = b,包含时似乎有100%的错误率,而排除它时似乎有0%的机会。


但是,OP表示这不是他观察到的行为。
奥利弗·查尔斯沃思

@OliCharlesworth然后,他必须处于不符合Java 5内存模型的Java运行时中,或者做错了什么。我自己测试一下,看看是否观察到相同的交互作用,我有一种不愿意的感觉
John Vint 2012年

1
是否将b == 0删除作为JIT优化?
约翰·温特

2
我使用了-XX:+ UnlockDiagnosticVMOptions -XX:+ PrintCompilation -XX:+ PrintAssembly,它是通过Eclipse调试(但也可以正常运行)在32位JDK6u30上运行(在64位计算机上)。
沃尔特·兰

2
我敢打赌这是一个OSR错误,它不是第一个发生的OSR错误(最常见的错误包括TieredCompilation w / c1-> c2和JVM崩溃)。
bestsss 2012年

12

基于下面的JCiP摘录,我会认为您的示例永远不要显示“错误”:

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


我会为此+1,因为这就是为什么我认为我们永远都看不到“错误”的原因……除了有些人报告他们确实看到了“错误”之外,所以这一定不适用!
yshavit 2012年

对于从未见过此错误的人...只是为了缩小范围...您正在测试哪种JVM和CPU?
cHao 2012年

2
@JohnVint地狱是的。有了-d32它,可靠地重现了问题。
Marko Topolnik

1
@MarkoTopolnik进入程序集时,它似乎失败了(尽管我可能错了),它引用的本地存储tt。其中“ b” == 0为真。
约翰·芬特

1
@JohnVint我正以[concurrency-interest]进入该线程,这只是引起麻烦的32位客户端。我已经在我的机器上通过-server进行了验证,但没有看到效果。
Marko Topolnik


-2

我认为,该问题是由于缺乏同步而引起的:

注意:如果b = 1在a = 1之前变大,而a在b不变时是易失的,那么b = 1实际上仅在a = 1完成后才针对所有线程更新(根据quate的逻辑)。

您的代码中出现的问题是,首先仅对主进程更新了b = 1,然后仅在易失性赋值完成时更新了所有线程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;
        }  
    }
}

4
不幸的是你不正确。有特定要求。看看g.oswego.edu/dl/jmm/cookbook.html。您将在该Can Reorder网格中注意到JMM持有的同步承诺。重要的部分是1.正常存储不能与后续的“可变存储”一起重新排序,以及2.可变负载不能与后续的“正常加载”一起重新排序。这两个示例都说明了
John Vint 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.