Java中的锁(Lock)、同步(synchronized)、原子变量(atomic variables)和易失性变量(volatile)在读写原子性、可见性和防止重排序方面的比较。

3

我从《Java Concurrency in Practice》这本书中了解到了volatile的以下内容:

当一个变量声明为volatile时,编译器和运行时会知道该变量是共享的并且它的操作不应与其他内存操作重新排序。volatile变量不会被缓存在寄存器或者其他处理器的缓存中,因此对volatile变量的读取总是返回任何线程最新的写入值。

volatile变量的可见性效果超出了volatile变量本身的值范围。当线程A写入一个volatile变量,并且随后线程B读取相同的变量时,A在写入volatile变量之前所有可见的变量值也将在B读取volatile变量之后变得可见。所以从内存可见性的角度来看,写入volatile变量就像退出同步块,而读取volatile变量就像进入同步块。

我对上面最后一句话很困惑。假设变量x被定义为volatile,并且在修改x之前,变量uvw对线程A可见,那么当线程B随后读取x时,它也将能够读取变量uvw的最新值。我们可以为synchronized指定相同的内容吗?

Q1.也就是说,下面的理解是否正确?

在退出synchronized块之前,变量uvw对线程A可见,那么当线程B随后进入synchronized块时,它将能够读取变量uvw的最新值。

我认为上述观点是不正确的,因为变量uvw可能存储在缓存和寄存器中,因为它们没有被定义为volatile。我的理解正确吗?所以synchronized(以及locksatomic变量,因为它们类似于synchronized)不能保证可见性。

该书进一步指出:

锁定可以同时保证可见性和原子性;volatile变量只能保证可见性。

但我认为以下内容:

  1. 锁、synchronized和原子变量仅保证读取-写入原子性(不保证可见性和防止重排序)。
  2. volatile保证可见性,并通过编译器和运行时防止重排序(不保证读取-写入原子性)。

Q2.我的上述两个观点是否正确?


1
你可能会发现Java语言规范中关于volatile字段的这一部分很有启发性。 - VGR
好的,我需要阅读它们。但是,你能否用最少的词回答我的问题?比如只用“是”或“不是”? - MsA
“_volatile_变量只能保证可见性。”不正确的是:对_volatile_变量的访问总是原子的。但是多次访问不能变成原子操作。只有锁定互斥量才能使一系列操作看起来是原子的,但这仅从使用相同互斥量的其他线程的角度来看。 - curiousguy
我猜测volatile并不能使变量修改原子化,因为Java并发实践这本书中说到:“volatile的语义不足以使增量操作(count++)原子化,除非你可以保证该变量只从单个线程中写入”。这是否意味着只有一个线程对volatile变量进行读写时是原子的,因为volatile确保不会对该线程进行重新排序,但是如果有其他线程修改同一变量,则不会是原子的,因为重新排序预防无法跨线程工作? - MsA
1
@anir,如果你读了我上面针对curiousguy的评论,请不要忘记这样一个声明i++并不是单一的“访问”。它的意思是:获取i的值,然后将递增的值存储回i中。也就是说,这是两个“访问”的序列。 - Solomon Slow
1
看起来我犯了一个错误:JLS承诺“对于longdouble类型的volatile变量进行的写入和读取操作始终是原子性的。”但我之前说的话(现在已删除)仍然基本正确:无论您是否将它们声明为volatile,对所有其他类型的变量进行的写入和读取操作都是原子的。只有在longdouble这种特殊情况下,volatile才会在原子性方面添加任何内容。 - Solomon Slow
2个回答

1

1) 锁、同步和原子变量保证读写的原子性可见性和防止重排序。

2) volatile保证了可见性和防止编译器和运行时的重排序。

volatile字段的读写原子性有点棘手:读写volatile字段是原子的,例如在32位jvm上写入volatile long(64位)时,读取和写入仍然是原子的。您始终会读取完整的64位。但是对于volatile int或long之类的操作不是原子的。


这本书中有这样一句话:“锁定可以保证可见性和原子性;而 volatile 变量只能保证可见性。” 我相信这明确表明了 volatile 变量不能确保原子性。我在这里提出了更具结构性的问题。此外,现在我只对synchronized的重新排序存在重大疑问,还有其他问题我已在新问题中提出。您能检查一下那个问题吗? - MsA

1
希望以下内容准确...这是我当前尽可能简单但不过于简单的理解....
Q1. 也就是说,以下正确吗?
变量 u、v 和 w 在退出 synchronized 块时对线程 A 可见,然后最新的 u、v 和 w 值将对进入 synchronized 块的线程 B 可见。
在这里我假设“最新值”实际上指的是“在退出 synchronized 块时的最新值”...
那么是的——当然必须注意,A 和 B 必须同步在相同的对象上。
副产品: 当然,对于 volatile 的可见性保证也适用类似的注意事项——A 和 B 必须写入和读取(分别)相同的 volatile 字段。
但是我有以下感觉:
1. 锁、synchronized 和原子变量只保证读写原子性(不保证可见性和防止重排序)。 2. volatile 保证可见性和防止编译器和运行时的重排序(不保证读写原子性)。
Q2. 我上面的两点正确吗?
对于第二点是正确的,但对于第一点是错误的...
synchronized 除了保证原子性之外还保证可见性。"可见性" 的概念也被描述为存在一个 happens-before 关系。
换句话说,线程 A 退出 synchronized (x) 发生在线程 B 进入 synchronized (x) 之前。
同样地,对 volatile 字段 x 的写入发生在对 volatile 字段 x 的读取之前。
换句话说,就可见性而言,“synchronized”进入/退出对提供与“volatile”读/写对完全相同的保证。但是,“synchronized”对同时保证了可见性和原子性,而“volatile”仅保证了可见性。哎呀-忘记了一个例外:“volatile long”和“volatile double”确保这些64位值的读取和写入是原子的(即将避免“字撕裂”)。另一种看待它的方法:拥有一个“volatile”字段“x”有点像在每次读或写“x”的时候都有一个微小的“synchronized(x')”,其中“x'”是某个与“x”对应的其他不可见锁对象(它并不完全相同,因为使用“volatile”必须将读取与写入配对,而所有的“synchronized”关键字都以相同的方式工作)。

我认为上述事实是不正确的,因为u、v和w可以存储在缓存和寄存器中,因为它们没有定义为volatile。我对此正确吗?

这有点令人惊讶,但是 synchronizedvolatile 提供的可见性保证适用于两个线程可见的所有内容,而不仅仅限于被锁定的对象、volatile 字段本身、同一对象中的其他字段等等。如果您熟悉低级汇编/内核编程等,则以内存屏障的方式思考它们似乎是有道理的。

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