易失性内存的目的和优势

5

volatile关键字在什么情况下使用?更重要的是:程序如何从中受益?

根据我所读和已知的,volatile应该用于被不同线程访问的变量,因为它们比非volatile的变量读取速度稍快。如果是这样,难道就没有强制使用相反的关键字吗?

或者它们实际上在所有线程之间进行了同步吗?普通变量为什么不行?

我有很多多线程代码,我想稍微优化一下。当然,我不希望有巨大的性能提升(目前我没有任何问题),但我一直在努力使我的代码更好。我对这个关键字有点困惑。


Volatile比同步更快,因为它不需要获取监视器锁定。如果您已经使用同步来完成所有操作,则可能并不重要。另一方面,在同步块之外是实现定义的Wild-Wild-West,并且不能保证任何一个线程看到其他线程正在执行的任何操作。Volatile可以做一些中等大小的事情,这里的答案已经开始涉及,但我建议如果您真的想很好地掌握它,就应该进行一段时间的研究。 - Radiodef
你真的想要阅读这篇文章来全面了解volatile是如何工作以及它实际上是做什么的。是的,它是针对C#的,但是Java和CLR之间的内存模型在这一点上非常相似。有趣的是,在CLR和JMM规范中都没有提到“从内存中读取最新值”的任何内容 - 排序保证几乎可以给您这个保证,但实际上还存在一些边缘情况。 - Voo
5个回答

8
当一个多线程程序运行时,如果存在一些未声明为volatile的共享变量,这些线程会创建该变量的本地副本,并在本地副本上进行操作。因此,变量的更改不会反映出来。这样做是因为缓存访问比从主内存访问变量快得多。
当你将一个变量声明为volatile时,它告诉程序不要创建任何变量的本地副本,直接从主内存中使用变量。
通过将变量声明为volatile,我们告诉系统它的值可能会意外地从任何地方改变,因此始终使用保存在主内存中的值,并且始终在主内存中更改变量的值,而不创建任何变量的本地副本。
请注意,volatile不能替代同步,并且当一个字段被声明为volatile时,编译器和运行时会注意到该变量是共享的,对它的操作不应与其他内存操作重新排序。volatile变量不会被缓存在寄存器或缓存中,因此读取volatile变量总是返回任何线程最近写入的值。

我从没用过 volatile,也没有遇到变量不及时更新的问题,所以我不太明白你所说的 变量的更改没有反映出来。那么,volatile 变量实际上会慢一些,但对于多线程来说仍然更快? - AyCe
1
一个volatile变量的访问速度与任何非volatile变量的访问速度一样快。但是相对于JVM为了快速执行Java程序而创建的缓存变量的访问,它的访问速度较慢。volatile变量仍然存在于主内存中,因此对于希望使用它的任何线程来说,它就像是变量的一个查找位置。如果任何变量不是volatile并且多个线程在使用它,那么它们可以自由地创建变量的缓存/本地副本,这样该变量现在就存在于内存中的多个位置,这可能不是程序员想要的结果。 - Aman Agnihotri

2

volatile关键字会使得每个线程都从内存中实际获取变量的值,因此访问变量会变慢,但能够获得最新的值。

当从不同的线程中访问变量时,这非常有用。

使用分析器来调整代码并阅读优化Java代码的技巧


好的,但是如果我每次更改多线程变量时都使用synchronized(这是我主要做的事情),那么它们会从volatile中受益吗?还是这是针对未明确锁定的变量的? - AyCe
Synchronized是volatile+锁定。使用volatile非常不寻常。 - Javier
@lalaland发布的链接(https://dev59.com/C3VD5IYBdhLWcg3wDXF3)很好,尽管有点技术性。 - Javier
1
如果您正确使用同步,可能不需要使用volatile。当您在对象上进行同步时,该块内部对共享内存所做的任何更改都会被其他线程立即看到,只要它们在相同的对象上进行同步。Volatile是一种在没有同步的情况下保证共享内存可见性的方法。 - Radiodef

2
< p > volatile 关键字意味着每次引用变量时,编译器都会强制读取新值。当该变量是标准内存之外的东西时,这很有用。例如,在嵌入式系统中,您正在读取一个硬件寄存器或接口,它对处理器而言就像一个内存位置。如果处理器使用先前读取的缓存值,则更改该寄存器值的外部系统更改将无法正确读取。使用volatile 强制重新读取并保持所有内容同步。


不错,但似乎一切都已经同步了。从我得到的所有答案来看,volatile 看起来并没有真正做任何 VM 本身无法完成的事情。但是,例如访问变量时,它会提高速度吗? - AyCe

1

这里有一个很好的Stack Overflow解释

这里有一篇好的维基文章

在计算机编程中,特别是在C、C++、C#和Java编程语言中,使用volatile关键字声明的变量或对象通常具有与优化和/或线程相关的特殊属性。一般来说,volatile关键字旨在防止编译器应用某些它本来可能会应用的优化,因为通常假定变量不能“自行”更改值。

**^维基

简而言之,它保证一个给定的线程访问某些数据的相同副本。一个线程中的任何更改都会立即在另一个线程中被注意到。


那么volatile只是一个“提示”,对吗?当当前代码太慢时可以考虑使用它?我仍然不确定应该声明哪些变量为volatile。你说的“这通常是不好的!”是什么意思? - AyCe
@AyCe 不,volatile不仅仅是一个提示。它有具体的规则。如果没有volatile,你可以在一个线程中对一个变量进行写入,而其他线程在理论上则被允许永远不看到这个写入。实际上,其他线程最终会看到这个写入,但是延迟足够长甚至可以被人感知。Volatile意味着其他线程将立即看到该写入(除了其他一些它所做的事情)。 - Radiodef
@Radiodef 我已经做了多线程的事情好几年了,从来没有观察到这样的行为。 :O 我想知道虚拟机是否只是对做错事情的人非常友好,还是我确实用 synchronized 做得很正确? - AyCe
可能两者都有。假设您已经正确使用了同步,那么事情可能是因为这样才起作用的。现实情况也是,线程通常会看到非易失性非同步写入,因此错误只看起来像是非常轻微的延迟。易失性还可以做一些同步无法做到的事情,即防止编译器重构涉及变量的语句。这可以防止非常微妙的错误,例如臭名昭著的双重检查锁定内联构造函数 - Radiodef
@Radiodef 我刚刚读了这个。目前我正在为一些只需要一个实例的类使用单例模式,通过具有静态实例和获取函数,在实例字段中存储新创建的对象,如果它为空。从我在那里读到的内容来看,似乎通过使用静态处理器将始终访问相同的内存,因此我不应该有这个问题。它没有同步,但是我可以根据我的代码顺序保证,在未访问之前,两个线程永远不会同时访问get-function。 - AyCe
听起来你所做的可能是安全的。(请参见导致发生在之前的操作. 具体而言,我在这里想到的是启动线程。)但值得注意的是,在Java中进行惰性实例化的首选方法是使用延迟初始化占位符技巧 - Radiodef

1

volatile关注内存可见性。在对volatile变量进行写操作后,该变量的值对所有读取者都变得可见。有点像关闭缓存。

这里是一个好的堆栈溢出响应: 你是否曾经在Java中使用过volatile关键字?

关于具体问题,它们没有被同步。您仍然需要使用锁定来完成。普通变量既不同步也不易失。

为了优化线程化代码,最好阅读粒度、乐观和悲观锁定方面的知识。


在我看来,volatile 只是承诺做虚拟机已经完成的事情。你链接帖子中第一个答案中的示例,我会使用 synchronized 来实现 - 在我看来更加简洁。 - AyCe
双重检查锁定是需要同时使用synchronizedvolatile的示例,可以在此处查看它们的应用:http://en.wikipedia.org/wiki/Double-checked_locking。 - lalaland
我刚刚读了一些维基百科的文章。到目前为止,似乎我不需要所描述的模式来满足我的意图 - 所有的惰性初始化都是线程安全的。但是,阅读“由于同步方法可能会使性能降低100倍或更高”的内容让我思考了一下我的当前代码,我没有想到影响会如此之大。我几乎在所有有意义的地方使用synchronized,但我没有预料到它会那么昂贵! - AyCe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接