Java:volatile如何保证这段代码中“数据”的可见性?

Posted

技术标签:

【中文标题】Java:volatile如何保证这段代码中“数据”的可见性?【英文标题】:Java: how volatile guarantee visibility of "data" in this piece of code? 【发布时间】:2016-02-12 00:53:54 【问题描述】:
Class Future

    private volatile boolean ready;
    private Object data;
    public Object get()
    
        if(!ready) return null;
        return data;
    

    public synchronized void setOnce(Object o)
    
        if(ready) throw...;
        data = o;
        ready = true;
    

它说“如果一个线程读取数据,则从写入到读取存在一个先发生边缘,以保证数据的可见性”

我从我的学习中知道:

    volatile 确保每次读/写都在内存中,而不仅仅是在缓存或寄存器中; volatile 确保重新排序:即在 setOnce() 方法中 data = o 只能安排在 if(ready) throw... 之后,并且在 ready = true 之前;这保证如果在 get() ready = true 中,数据必须为 o。

我的困惑是

    有没有可能当线程1在setOnce()中时,到达data = o之后的那个点;准备好之前=真;同时线程2运行get(),read ready为false,返回null。并且 thead 1 继续准备好 = true。 在这种情况下,即使数据已在线程 1 中分配了新值,线程 2 也没有看到新的“数据”。

    get() 未同步,这意味着同步锁无法保护 setOnce(),因为线程 1 调用 get() 无需获取锁即可访问变量就绪数据。所以线程不能保证看到最新的数据值。我的意思是锁只保证同步块之间的可见性。即使一个线程正在运行同步块 setOnce(),另一个线程仍然可以进入 get() 并在不阻塞的情况下访问就绪和数据,并且可能会看到这些变量的旧值。

    在 get() 中,如果 ready = true,数据必须为 o?我的意思是这个线程可以保证看到数据的可见性?我认为 data 不是 volatile 也不是 get() 同步的。这个线程是否可以看到缓存中的旧值?

谢谢!

【问题讨论】:

这是什么语言?爪哇? 另外,您的1 大多是错误的。 volatile 关键字与内存可见性有关,而不是缓存。缓存由缓存一致性硬件处理。这显然是一个没有人会使用的糟糕设计——内存太慢了,无法以这种方式使用。 @DavidSchwartz 在 Java 中可以将变量存储在缓存中。 L1 和 L2 缓存对于不同的线程是不可见的,使用 volatile 将值存储在主内存或 L3 缓存中(主内存和 L3 缓存在线程之间共享)。 More info @VelkoGeorgiev 这完全是错误的。这不是缓存的工作方式。这是一个普遍的神话,但它只是一个神话。 volatile 关键字与这些缓存没有任何关系。对volatile 的访问可以完全保留在 L1 缓存中,没有任何问题。 (遗憾的是,您链接到的文章重复了这个神话。) @VelkoGeorgiev 我在文章上做了一些 cmets。当一个人如此彻底地误解了一个重要问题并试图将其教给其他人时,这令人愤怒。 【参考方案1】:

volatile 确保每次读/写都在内存中,而不仅仅是在缓存或寄存器中;

不。它只是确保它对其他线程可见。在现代硬件上,这不需要访问内存。 (这是好事,主存很慢。)

volatile 确保重新排序:即在 setOnce() 方法中 data = o 只能安排在 if(ready) throw... 之后,并且在 ready = true 之前;这保证了如果在 get() ready = true 中,数据必须是 o。

没错。

有没有可能当线程1在setOnce()中时,到达data = o之后的那个点;准备好之前=真;同时线程2运行get(),read ready为false,返回null。并且 thead 1 继续准备好 = true。在这种情况下,线程 2 看不到新的“数据”,即使数据已在线程 1 中分配了新值。

是的,但如果这是一个问题,那么你不应该使用这样的代码。据推测,此代码的 API 将保证 getsetOnce 返回后调用时可以看到结果。显然,你不能保证get 在我们完成之前就能看到结果。

get() 不是同步的,这意味着同步锁不能保护 setOnce(),因为线程 1 调用 get() 不需要获取锁来访问变量准备好的数据。所以线程不能保证看到最新的数据值。我的意思是锁只保证同步块之间的可见性。即使一个线程正在运行同步块 setOnce(),另一个线程仍然可以进入 get() 并无阻塞地访问就绪和数据,并且可能会看到这些变量的旧值。

没有。如果这是真的,同步将几乎不可能使用。例如,一种常见的模式是创建一个对象,然后获取集合上的锁并将对象添加到集合中。如果获取集合上的锁并不能保证创建对象所涉及的写入是可见的,那么这将不起作用。

在get()中,如果ready = true,数据必须是o?我的意思是这个线程可以保证看到数据的可见性?我认为 data 不是 volatile 也不是 get() 同步的。这个线程可能会看到缓存中的旧值吗?

Java 的volatile 操作是这样定义的,这样可以保证看到对一个更改的线程可以看到所有其他内存更改,该线程在线程看到的更改之前进行了更改。这在其他语言(例如 C 或 C++)中是不正确的。这可能会使 Java 的 volatile 在某些平台上变得更加昂贵,但幸运的是在典型平台上不会。

另外,请不要谈论“在缓存中”。这与缓存无关。这是一个常见的误解。它与可见性有关,而不是缓存。大多数缓存提供对缓存的完全可见性(将“MESI 协议”插入您最喜欢的搜索引擎以了解更多信息),并且不需要任何特殊的东西来确保可见性。

【讨论】:

首先非常感谢您抽出宝贵时间帮我解答详细问题! 问题 1 现已解决。但是对于第二个问题,我从文档“当线程释放内部锁时,在该操作与任何后续获取相同锁之间建立了先发生关系”中读到了这一点。这让我感到困惑。我在想你关于收藏的例子。如果 t1 在同步的 add() 由 t2 运行时通过非同步方法 change() 对集合进行一些更改,会发生什么情况。 t1 不会阻塞,因为这是一个同步方法。 t1 是否会看到添加的集合或其他内容? @ChristyLin 这种发生之前的关系意味着该线程之前所做的任何事情都会释放锁,这对于以后获得相同锁的任何线程都是可见的。您需要同步对集合的所有访问才能使其正常工作。在Java 中,volatile 建立了同样的关系——如果一个线程修改了任何volatile 变量,看到该修改的线程也将看到在该修改之前所做的所有修改。同样,这是特定于 Java 的。 现在我明白了 volatile 带来的影响。但是假设集合类不涉及任何 volatile 变量,它只有一个同步的 add() 和一个不同步的 change(),那么 add() 不能保证对 change() 可见,对吧?如果我需要这两个方法彼此可见,我必须让这两个都同步,对吧? 非常感谢!现在我所有的困惑都消失了。很高兴!祝你有美好的一天!

以上是关于Java:volatile如何保证这段代码中“数据”的可见性?的主要内容,如果未能解决你的问题,请参考以下文章

java面试-谈谈你对volatile的理解

java volatile 关键字

JAVA并发编程递进篇,探索线程安全性volatile关键字如何保证可见性

JAVA并发编程递进篇,探索线程安全性volatile关键字如何保证可见性

Java基础:volatile详解

java中volatile不能保证线程安全(实例讲解)