相关问题

  1. volatile 是什么?它主要解决什么问题?
  2. volatile 为什么能保证可见性?
  3. volatile 为什么能保证有序性?
  4. volatile 能保证原子性吗?为什么 count++ 仍然线程不安全?
  5. volatile 和 synchronized 有什么区别?
  6. volatile 的典型使用场景有哪些?
  7. 双重检查单例为什么一定要加 volatile?
  8. 什么是 happens-before?volatile 的 happens-before 规则是什么?
  9. volatile 底层是怎么实现的?内存屏障起什么作用?
  10. CAS 是什么?Compare And Swap 比较和交换的三个值分别是什么?
  11. CAS 为什么能保证一次更新操作的原子性?
  12. CAS 和自旋是什么关系?
  13. CAS 为什么被称为无锁并发?
  14. CAS 失败以后会发生什么?为什么高竞争下会浪费 CPU?
  15. 什么是 ABA 问题?
  16. ABA 为什么有时候会导致问题?
  17. ABA 怎么解决?版本号和 AtomicStampedReference 是怎么做的?
  18. CAS 有哪些局限?
    • 高竞争下自旋开销
    • ABA
    • 单变量更新更合适
  19. AtomicInteger 为什么线程安全?底层和 CAS 有什么关系?
  20. AtomicInteger.incrementAndGet() 大致是怎么实现的?
  21. CAS 和 synchronized / ReentrantLock 有什么区别?
  22. CAS 适合什么场景,锁又适合什么场景?
  23. 为什么 AQS、Atomic 类、轻量级锁里都会大量使用 CAS?
  24. LongAdder 为什么在高并发下通常比 AtomicLong 更合适?
  25. LongAdder 为什么最终值不是每次更新时都集中写到一个变量上?

volatile 的作用

volatile 是 Java 提供的一种轻量级同步机制,主要解决两个问题:可见性和有序性。

多线程环境下,一个线程修改了共享变量以后,其他线程并不一定能立刻看到最新值。

原因是现代 CPU 并不是每次读写变量都直接访问主内存。每个 CPU 核心都有自己的缓存,线程运行时可能会直接从当前核心的缓存里读取变量。

比如:

1
2
主内存:
flag = true

线程 A 和线程 B 都读过这个变量以后,可能分别在自己的 CPU 缓存中保存一份:

1
2
CPU 1:flag = true
CPU 2:flag = true

这时线程 A 修改:

1
flag = false

线程 B 可能仍然继续读取自己缓存里的旧值 true。

这就是所谓的可见性问题:一个线程对共享变量的修改,其他线程什么时候能够看到,并不是天然有保证的。

如果变量使用 volatile 修饰:

1
volatile boolean flag = true;

那么一个线程对 flag 的写入,对后续读取这个 volatile 变量的其他线程具有可见性保证。

除了可见性以外,volatile 还会限制指令重排序。

编译器和 CPU 为了提高执行效率,在不影响单线程执行结果的情况下,可能会调整部分指令的执行顺序。

单线程环境下通常没有问题,但多线程环境中,其他线程可能观察到这个被重排后的执行顺序。

例如:

1
instance = new Singleton();

创建对象可以粗略理解为:

1
2
3
分配内存
→ 初始化对象
→ instance 指向对象

如果发生指令重排序,就可能变成:

1
2
3
分配内存
→ instance 指向对象
→ 初始化对象

这时候另一个线程可能已经看到:

1
instance != null

但实际上对象还没有初始化完成,于是就可能使用到一个处于不完整状态的对象。

因此,多线程环境下不仅要保证一个线程修改的数据能够被其他线程看到,还要保证一些关键操作不能被随意调整顺序。

volatile 会通过内存语义限制相关指令的重排序,从而保证一定的有序性。

不过 volatile 不能保证复合操作的原子性。

例如:

1
2
3
volatile int count = 0;

count++;

count++ 仍然不是线程安全的,因为它实际上包含读取、计算、写回多个步骤。

所以 volatile 主要保证的是:

1
2
可见性
有序性

而不能替代锁去保证复合操作的原子性。

另外,volatile 也不能简单理解成“每次都直接读写主内存”。更准确地说,是 Java 内存模型规定了它的可见性和有序性语义,JVM 再通过内存屏障以及底层 CPU 的缓存一致性机制去实现这些保证。

从 JMM 的角度看,对一个 volatile 变量的写 happens-before 后续对这个变量的读,因此前面对共享数据的修改,对后续读线程可见。

为什么 DCL 需要 volatile

DCL,也就是双重检查锁单例,通常会写成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private static volatile Singleton instance;

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

这里 volatile 最核心的作用是防止对象创建过程发生指令重排序。

这一句:

1
instance = new Singleton();

可以粗略理解成三个步骤:

1
2
3
分配内存
→ 初始化对象
→ instance 指向这块内存

如果没有 volatile,后两步可能发生重排序:

1
2
3
分配内存
→ instance 先指向这块内存
→ 再初始化对象

这样就可能出现一种情况:线程 A 还没有把对象初始化完成,但 instance 已经不是 null 了。

这时线程 B 执行外层判断,发现:

1
instance != null

就会直接返回这个对象,从而拿到一个还没有初始化完成的实例。

因此 DCL 中使用 volatile,主要就是为了禁止这种关键的指令重排序,同时保证 instance 的写入对其他线程可见。

所以可以记住:

DCL 中 synchronized 保证只有一个线程负责创建对象,volatile 保证其他线程不会看到一个半初始化的对象。

CAS

CAS,全称 Compare And Swap,也就是比较并交换。

它的核心思想是:修改一个值之前,先判断这个值是不是还是自己之前读取到的值,如果是就更新,否则说明期间已经被其他线程修改过。

CAS 通常涉及三个值:

1
2
3
当前值
期望值
新值

如果当前值等于期望值,就把它更新成新值;如果不相等,这次 CAS 就失败。

CAS 经常会配合自旋使用:失败以后重新读取最新值,再继续尝试,直到成功或者主动放弃。

它的优势是可以在不加互斥锁的情况下完成一些原子更新,所以 AtomicInteger、AQS 等并发组件都会大量使用 CAS。

CAS 也有一些局限。

最典型的是 ABA 问题:一个值从 A 变成 B,又变回 A,CAS 最后看到的还是 A,就无法知道中间发生过变化。可以通过版本号解决,例如 AtomicStampedReference。

另外,高竞争下 CAS 可能频繁失败并不断自旋,持续占用 CPU;而且它更适合单变量或简单状态更新,如果一次操作涉及多个相关状态,直接使用 CAS 会比较复杂。