Java-并发.04.Volatile

volatile关键字特性

  • 多CPU环境的可见性: 多CPU的环境下, CPU有可能从寄存器或Cache里直接取值, 这种情况下运行在不同CPU上的线程获取的值可能不同, volitile变量可以保证每次更新都改变到主存, 每次读取都从主存中读取.
  • volatile 可以作为一种开销较低的免锁机制(某些情况下).
  • volatile 的 long, double 的读写不保证有原子性.
  • volatile变量的”复合操作”(对变量的写操作依赖当前值)不具备原子性.

volatile 的应用

➤ 适用的情况:

  1. 作为简单的状态标志, vol_variable = 1vol_variable = 0 这种操作是原子的, 对 volatile 变量的赋值也对其他线程立刻可见;
  2. 保证只有一个线程写, 其他线程只能读;

➤ 不适用的情况:

  1. 用于计数器(请使用AomicInteger), 虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由 “读取-修改-写入” 操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。
  2. “依赖当前值”的写操作, 比如i=i+1
  3. 非原子操作, i++, i=!i都不是原子操作

比如以下代码是有问题的:

private volatile i = 0;
protect void filp() {
i = !i;
}

Volatile 的特性是如何实现的?

  • 第一:使用 volatile 关键字会强制将修改的值立即写入主存;
  • 第二:使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程1的工作内存中缓存变量的 缓存行 无效(缓存行,CPU 高速缓存中的 cache line)
  • 第三:由于线程1的工作内存中缓存变量的 缓存行 无效,所以线程 1 再次读取变量的值时会去主存读取。

上面的特性,是通过 内存屏障 实现的,详见 Java-并发.09.深入理解JMM

Volatile Double 是原子的吗?

先看硬件的支持,如果要保证 double 写的原子性,那么要满足 64bit + 内存对齐 + 它没跨 cache line

  • 如果是 64bit,且这个 double 是内存对齐的,那么对它的读写是原子的,但是很多情况下(C++ 和 Java 中)无法保证对齐,也就无法保证原子性
  • 根据 IA64 手册,X86_64 架构下,不跨越 cacheline 的 8byte 读写是原子的

Double 是否是原子性,1 看硬件(64),2 看上层是否实现了对 double 的对齐

Java 语言规范文档:jls-17 Non-Atomic Treatment of double and long

  1. 对于 64 位的 long 和 double,如果没有被 volatile 修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对 32位操作。
  2. 如果使用 volatile 修饰 long 和 double,那么其读写都是原子操作
  3. 在实现 JVM 时,可以自由选择是否把读写 long 和 double 作为原子操作

作者测试了 32 Hotspot 和 64 下多线程写 volatile double 的表现:

  • 32:无法保证原子性
  • 64:看结果是可以保证