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

Posted

技术标签:

【中文标题】为啥 java 5+ 中的 volatile 不能确保来自另一个线程的可见性?【英文标题】:Why doesn't volatile in java 5+ ensure visibility from another thread?为什么 java 5+ 中的 volatile 不能确保来自另一个线程的可见性? 【发布时间】:2012-05-24 03:24:36 【问题描述】:

根据:

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;
    

a 为 1 时,

b 应该对所有线程都为 1。

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

【问题讨论】:

@OliCharlesworth 我想他在问为什么b 的各种缓存值在对a 进行易失性写入/读取后不同步到b=1 您是否真的在 Java 1.5+ 中运行过该代码并看到打印的“错误”? @OfekRon 根据 Java 内存模型,b 是否为 volatile 并不重要,因为写入它之后是写入 volatile var,而在另一个线程中读取它之前会读取相同的 volatile var。 Java 并发兴趣电子邮件列表中正在讨论此线程:cs.oswego.edu/pipermail/concurrency-interest/2012-May/… 只是从并发兴趣列表中快速更新,看起来这是在最新的 Java7 中修复的:download.java.net/jdk7u6/changes/jdk7u6-b14.html(查看热点部分的最后一个条目。错误 ID 链接到用你的用例报告错误。 【参考方案1】:

更新:

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

Report Changeset Buglist

原答案

在考虑内存可见性/顺序时,您需要考虑其发生前的关系。 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 的staticget 似乎有很大的影响(为了证明这删除了int tt = b;,它应该总是有效的。

看来b 加载到tt 将在本地存储该字段,然后将在if coniditonal 中使用该字段(对该值的引用不是tt)。因此,如果b == 0 为真,则可能意味着tt 的本地存储为0(此时将1 分配给本地tt)。这似乎只适用于带有客户端集的 32 位 Java 1.6 和 7。

我比较了两个输出程序集,直接的区别就在这里。 (请记住,这些是 sn-ps)。

这个打印的“错误”

 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

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

【讨论】:

但是 OP 说这不是他观察到的行为。 @OliCharlesworth 那么他一定是在不符合 Java 5 内存模型的 Java 运行时上,或者做错了什么。我会自己测试一下,看看是否观察到相同的交互,我有一种感觉我不会 是否删除 b == 0 作为 JIT 优化? 我使用了 -XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintAssembly,使用 JDK6u30 32 位(在 64 位机器上)从 Eclipse 调试(但也可以正常运行)运行。 我敢打赌这是一个 OSR 错误,它不是第一个发生的 OSR 错误(最喜欢的包括带有 c1->c2 的分层编译和 JVM 崩溃)。【参考方案2】:

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

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

【讨论】:

我会 +1 这个,因为这就是为什么我认为我们永远看不到“错误”......除了有些人报告他们确实看到了“错误”,所以这一定不适用! 对于那些从未见过这个错误的人......只是为了缩小范围......你在什么 JVM 和 CPU 上测试? @JohnVint Hell 是的。使用-d32,它可靠地重现了问题。 @MarkoTopolnik 查看程序集,看起来(尽管我可能是错的)当它失败时它引用了tt 的本地存储。其中 'b'==0 为真。 @JohnVint 我正在 [concurrency-interest] 中进入那个线程,它只是 32 位客户端造成了麻烦。我已经在我的机器上验证过了,用-server我没有看到效果。【参考方案3】:

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

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

【讨论】:

【参考方案4】:

在我看来,问题是由于缺乏同步

注意:如果 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;
          
    

【讨论】:

很遗憾你是不正确的。有具体要求。看看g.oswego.edu/dl/jmm/cookbook.html。您会在 Can Reorder 网格中注意到 JMM 持有的同步承诺。重要的部分是 1. Normal Store 不能用后续的 Volatile Store 重新排序,2. Volatile Load 不能用后续的 Normal Load 重新排序。这两个例子都说明了

以上是关于为啥 java 5+ 中的 volatile 不能确保来自另一个线程的可见性?的主要内容,如果未能解决你的问题,请参考以下文章

java单例双重检查锁为啥需要加volatile关键字

Java并发编程之验证volatile不能保证原子性

70%的Java程序员不知道为啥 ConcurrentHashMap 读操作不需要加锁?

Java中的volatile关键字为什么不是不具有原子性

为啥将 volatile 与同步块一起使用?

为啥我们使用 volatile 关键字? [复制]