一文解决内存屏障 volatile 和 内存屏障 Java 中的伪共享 (false sharing)

volatile 概念

volatile

  • 保证可见性
  • 保证有序性
  • 不保证原子性

volatile 是 Java 提供的一种稍弱的同步机制.
volatile 只能用于修饰类变量和实例变量.

volatile 变量具备两种特性:

  • 保证此变量对所有的线程的可见性
  • 禁止指令重排序优化

image.png

内存屏障 (volatile 原理)

image.png

思考: Java volatile 提供了什么保证? 底层怎么实现?
volatile 保证内存可见性, 禁止指令重排序, 但不保证原子性. 可见性是指, 一个线程的操作结果对于另一个线程来说是可见的.
实现原理是内存屏障, 内存屏障解决了可见性和指令重排序问题.

image.png

CPU 与内存之间有多级 cache 层, 每个 CPU 有自己的 cache 层.
单核 CPU, 不管变量 x 是否是 volatile 的, 线程 A 对 x 的操作结果对线程 B 来说都是可见的, 因为这两个线程看到的缓冲和内存是一样的 (先不考虑指令重排序的影响).
多核 CPU, 各个 CPU 缓冲层数据是不一致的, 非 volatile 变量 x, CPU-1 上的线程对 x 的操作结果对 CPU-2 上的线程不可见.

考虑操作 Object o = new Object(), 由于 new 不是原子操作, 可分为三个步骤:

  1. 分配一个内存块
  2. 初始化对象
  3. 将对象引用赋值给变量

由于指令重排序的存在, 实际的操作顺序可能是: 132. 如果只保证可见性, 而没有禁止指令重排序. 其他线程可能看到一个没有初始化完全的对象. 所以, volatile 关键字, 保证可见性的同时, 要禁止指令重排序.

对于 volatile 变量, JVM 会在写操作之后插入一个写屏障指令, 在读操作之前插入一个读屏障指令.

image.png

伪共享问题

Hardware keeps track of shared data at a fixed granularity, often in units of a cache entry of 32 or 64 bytes. This reduces hardware management overhead, but it can cause performance problems if multiple data structures with different sharing behavior fit in the same cache entry. This is called false sharing.

因为 CPU 缓冲是以缓冲行为单位进行操作的 (缓冲行通常为 64 字节).
假设 volatile long 型 x, y 在同一个缓冲行中. volatile 会让变量所在的缓冲行失效. CPU-1 上的线程操作 x, 同时 CPU-2 上的线程操作 y, 就产生了竞争冲突.
表面上 x 和 y 是被独立线程操作的, 而且两操作之间也没有任何关系, 但是 x, y 共享了一个缓存行, 产生竞争, 影响了性能.

伪共享是处理并发底层细节时一种经常需要考虑的问题, 现代处理器的缓存系统中是以缓存行 (Cache Line) 为单位存储的, 当多线程修改互相独立的变量时, 如果这些变量恰好共享同一个缓存行, 就会彼此影响 (写回, 无效化或者同步) 而导致性能降低, 这就是伪共享问题.

解决伪共享最直接的方法是: 字节填充. 即填充一些无用的字节, 将缓冲行填满.

  1. public class VolatileLong {
  2. private volatile long v;
  3. private long v0, v1, v2, v3, v4, v5; // 无用字节
  4. }

Java 8 提供了 @Contended 注解可以用于类型上和属性上, 加上这个注解之后虚拟机会自动进行填充, 从而避免伪共享.
这个注解在 Java 8 的 ConcurrentHashMap, ForkJoinPool, Thread 等类中都有应用.