相关问题
volatile是什么?它主要解决什么问题?volatile为什么能保证可见性?volatile为什么能保证有序性?volatile能保证原子性吗?为什么count++仍然线程不安全?volatile和synchronized有什么区别?volatile的典型使用场景有哪些?- 双重检查单例为什么一定要加
volatile? - 什么是 happens-before?
volatile的 happens-before 规则是什么? volatile底层是怎么实现的?内存屏障起什么作用?- CAS 是什么?Compare And Swap 比较和交换的三个值分别是什么?
- CAS 为什么能保证一次更新操作的原子性?
- CAS 和自旋是什么关系?
- CAS 为什么被称为无锁并发?
- CAS 失败以后会发生什么?为什么高竞争下会浪费 CPU?
- 什么是 ABA 问题?
- ABA 为什么有时候会导致问题?
- ABA 怎么解决?版本号和
AtomicStampedReference是怎么做的? - CAS 有哪些局限?
- 高竞争下自旋开销
- ABA
- 单变量更新更合适
AtomicInteger为什么线程安全?底层和 CAS 有什么关系?AtomicInteger.incrementAndGet()大致是怎么实现的?- CAS 和
synchronized / ReentrantLock有什么区别? - CAS 适合什么场景,锁又适合什么场景?
- 为什么 AQS、Atomic 类、轻量级锁里都会大量使用 CAS?
LongAdder为什么在高并发下通常比AtomicLong更合适?LongAdder为什么最终值不是每次更新时都集中写到一个变量上?
volatile 的作用
volatile 是 Java 提供的一种轻量级同步机制,主要解决两个问题:可见性和有序性。
多线程环境下,一个线程修改了共享变量以后,其他线程并不一定能立刻看到最新值。
原因是现代 CPU 并不是每次读写变量都直接访问主内存。每个 CPU 核心都有自己的缓存,线程运行时可能会直接从当前核心的缓存里读取变量。
比如:
|
|
线程 A 和线程 B 都读过这个变量以后,可能分别在自己的 CPU 缓存中保存一份:
|
|
这时线程 A 修改:
|
|
线程 B 可能仍然继续读取自己缓存里的旧值 true。
这就是所谓的可见性问题:一个线程对共享变量的修改,其他线程什么时候能够看到,并不是天然有保证的。
如果变量使用 volatile 修饰:
|
|
那么一个线程对 flag 的写入,对后续读取这个 volatile 变量的其他线程具有可见性保证。
除了可见性以外,volatile 还会限制指令重排序。
编译器和 CPU 为了提高执行效率,在不影响单线程执行结果的情况下,可能会调整部分指令的执行顺序。
单线程环境下通常没有问题,但多线程环境中,其他线程可能观察到这个被重排后的执行顺序。
例如:
|
|
创建对象可以粗略理解为:
|
|
如果发生指令重排序,就可能变成:
|
|
这时候另一个线程可能已经看到:
|
|
但实际上对象还没有初始化完成,于是就可能使用到一个处于不完整状态的对象。
因此,多线程环境下不仅要保证一个线程修改的数据能够被其他线程看到,还要保证一些关键操作不能被随意调整顺序。
volatile 会通过内存语义限制相关指令的重排序,从而保证一定的有序性。
不过 volatile 不能保证复合操作的原子性。
例如:
|
|
count++ 仍然不是线程安全的,因为它实际上包含读取、计算、写回多个步骤。
所以 volatile 主要保证的是:
|
|
而不能替代锁去保证复合操作的原子性。
另外,volatile 也不能简单理解成“每次都直接读写主内存”。更准确地说,是 Java 内存模型规定了它的可见性和有序性语义,JVM 再通过内存屏障以及底层 CPU 的缓存一致性机制去实现这些保证。
从 JMM 的角度看,对一个 volatile 变量的写 happens-before 后续对这个变量的读,因此前面对共享数据的修改,对后续读线程可见。
为什么 DCL 需要 volatile
DCL,也就是双重检查锁单例,通常会写成:
|
|
这里 volatile 最核心的作用是防止对象创建过程发生指令重排序。
这一句:
|
|
可以粗略理解成三个步骤:
|
|
如果没有 volatile,后两步可能发生重排序:
|
|
这样就可能出现一种情况:线程 A 还没有把对象初始化完成,但 instance 已经不是 null 了。
这时线程 B 执行外层判断,发现:
|
|
就会直接返回这个对象,从而拿到一个还没有初始化完成的实例。
因此 DCL 中使用 volatile,主要就是为了禁止这种关键的指令重排序,同时保证 instance 的写入对其他线程可见。
所以可以记住:
DCL 中 synchronized 保证只有一个线程负责创建对象,volatile 保证其他线程不会看到一个半初始化的对象。
CAS
CAS,全称 Compare And Swap,也就是比较并交换。
它的核心思想是:修改一个值之前,先判断这个值是不是还是自己之前读取到的值,如果是就更新,否则说明期间已经被其他线程修改过。
CAS 通常涉及三个值:
|
|
如果当前值等于期望值,就把它更新成新值;如果不相等,这次 CAS 就失败。
CAS 经常会配合自旋使用:失败以后重新读取最新值,再继续尝试,直到成功或者主动放弃。
它的优势是可以在不加互斥锁的情况下完成一些原子更新,所以 AtomicInteger、AQS 等并发组件都会大量使用 CAS。
CAS 也有一些局限。
最典型的是 ABA 问题:一个值从 A 变成 B,又变回 A,CAS 最后看到的还是 A,就无法知道中间发生过变化。可以通过版本号解决,例如 AtomicStampedReference。
另外,高竞争下 CAS 可能频繁失败并不断自旋,持续占用 CPU;而且它更适合单变量或简单状态更新,如果一次操作涉及多个相关状态,直接使用 CAS 会比较复杂。