Java无锁并发编程:volatile + CAS 原子类深度解析
1. 引言为什么需要无锁并发在 Java 并发编程的演进过程中synchronized关键字基于操作系统的互斥量 Mutex Lock是最初的解决方案。然而互斥锁属于悲观锁它假设并发冲突一定会发生因此会让未获取到锁的线程进入阻塞状态进而触发用户态与内核态的切换产生巨大的上下文切换开销。随着多核处理器的普及业界开始探索无锁并发Non-blocking Concurrency方案。无锁编程利用硬件底层的原子指令如 CPU 的CAS指令在不加锁的情况下保证数据的一致性和线程安全性。其核心优势在于无阻塞线程不会因为锁的竞争而被挂起避免了上下文切换。高吞吐在低到中等竞争下性能远优于锁。无死锁由于不持有锁自然不存在死锁风险。Java 实现无锁并发的两大支柱是volatile与CAS原子类。volatile保证了可见性和有序性CAS保证了原子性。2. Java内存模型JMM与可见性基石在深入volatile之前必须理解 Java 内存模型它定义了 JVM 在计算机内存RAM与 CPU 寄存器/缓存之间如何工作。2.1 硬件层CPU缓存架构与缓存一致性现代 CPU 的处理速度远超内存的读写速度为了弥补这个鸿沟CPU 引入了L1/L2/L3 三级缓存。MESI 协议当多个 CPU 核心同时操作同一块内存数据时如何保证缓存的一致性这依赖于缓存一致性协议。MESIModified, Exclusive, Shared, Invalid是其中最著名的协议。当某个核心修改了变量该核心的缓存行状态变为 Modified。协议会通过总线嗅探机制通知其他核心将其对应的缓存行状态标记为 Invalid。然而由于Store Buffer和Invalidate Queue的存在即使有 MESI 协议写操作并不会立刻通知其他核心这就导致了可见性问题的根源。2.2 JMM的抽象结构JMM 定义了主内存所有变量存储在主内存中堆内存区域。工作内存每个线程拥有自己的工作内存是对 CPU 寄存器和缓存的抽象。线程对变量的所有操作读、写都必须在工作内存中进行不能直接读写主内存。这就导致了如果线程 A 修改了变量在没有同步机制的情况下线程 B 可能看不到这个修改。2.3 happens-before 原则JMM 通过 happens-before 原则来界定哪些操作是线程安全的。如果两个操作不具备 happens-before 关系JVM 可以自由重排序它们。与volatile相关的规则是对一个 volatile 变量的写操作happens-before 于后续对这个 volatile 变量的读操作。3. volatile 关键字深度解析3.1 volatile 的两层语义可见性保证不同线程对这个变量进行操作时的可见性。即一个线程修改了 volatile 变量的值新值对其他线程立即可见。有序性禁止指令重排序优化。JVM 在编译和执行代码时为了性能会进行指令重排序volatile会插入特定的内存屏障来禁止重排序。3.2 禁止指令重排序内存屏障Memory Barrier为了实现volatile的语义JVM 在生成字节码时会在操作前后插入内存屏障。内存屏障是一种 CPU 指令它强制刷新 CPU 缓存并且禁止屏障前后的指令穿越屏障。JMM 对 volatile 的内存屏障插入策略以 JSR-133 为标准写屏障在每个volatile写操作前插入StoreStore屏障确保之前的普通写操作已刷入主存。在每个volatile写操作后插入StoreLoad屏障防止 volatile 写与之后可能出现的 volatile 读/写重排序。读屏障在每个volatile读操作后插入LoadLoad屏障 和LoadStore屏障确保后续的普通读/写操作不会重排序到 volatile 读之前。示例分析双重检查锁DCL单例模式javapublic class Singleton { private static volatile Singleton instance; // volatile 是关键 public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); // 核心操作 } } } return instance; } }如果instance不加volatileinstance new Singleton()这一步在 JVM 中大致分为三步分配内存空间。初始化对象。将instance引用指向内存空间。由于指令重排序步骤 2 和 3 可能交换顺序。当线程 A 执行到步骤 3引用已赋值但对象尚未初始化时线程 B 执行if (instance null)发现不为 null直接返回一个未初始化的对象导致程序崩溃。volatile禁止了这种重排序。3.3 volatile 的底层实现字节码与汇编字节码层面被volatile修饰的字段在 Class 文件中会有一个ACC_VOLATILE访问标志。JVM 层面HotSpot VM 在解释执行或 JIT 编译时遇到ACC_VOLATILE会插入内存屏障。汇编层面最终在 x86 架构下volatile写操作会在指令前加上lock前缀。assembly// 示例volatile 写操作对应的汇编指令 lock addl $0x0, (%rsp) // lock 前缀在 x86 中强制将 CPU 的缓存行写入内存并引发缓存一致性协议MESI失效。3.4 volatile 的局限性不保证原子性虽然volatile保证了可见性和有序性但它不保证复合操作的原子性。例如count读-改-写操作即使 count 是volatile也无法保证线程安全。解决方案必须配合 CAS 或者锁来保证原子性。4. CASCompare And Swap无锁算法4.1 CAS 的核心思想CAS 是一种乐观锁技术。它包含三个操作数V要更新的变量内存地址。A预期原值期望的旧值。B新值。逻辑只有当 V 的值等于 A 时才将 V 的值更新为 B否则什么都不做。无论成功与否都会返回 V 的当前值。CAS 是一条 CPU 并发原语它的执行是原子的不会出现数据不一致的问题。4.2 Unsafe 类Java 的“后门”Java 中的原子类底层依赖sun.misc.Unsafe类。Unsafe 提供了直接操作内存堆外内存、对象字段偏移量、以及 CAS 操作的能力。核心方法javapublic final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); public final native boolean compareAndSwapInt(Object o, long offset, int expected, int update); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);o对象本身。offset变量在该对象内存中的偏移量通过objectFieldOffset获得用于直接定位内存地址。获取 Unsafe 实例通常通过反射因为其构造器私有且getUnsafe()有类加载器限制javaField field Unsafe.class.getDeclaredField(theUnsafe); field.setAccessible(true); Unsafe unsafe (Unsafe) field.get(null);4.3 CAS 的三大问题及解决方案4.3.1 ABA 问题现象线程 T1 读取变量值为 A此时线程 T2 将值改为 B又改回 AT1 进行 CAS 操作时发现值仍为 A认为没有变化CAS 成功。但事实上变量已经历了“A-B-A”的变动可能已经产生了业务逻辑上的副作用。解决方案使用版本号/时间戳。Java 提供了AtomicStampedReference和AtomicMarkableReference。AtomicStampedReference内部维护一个[reference, integer]对CAS 时同时比较引用和 Stamp版本号每次修改都更新版本号从而解决 ABA 问题。4.3.2 循环时间长开销大现象在高并发下CAS 操作可能长时间自旋一直循环重试占用大量 CPU 资源。解决方案自适应自旋如LongAdder当竞争激烈时不再自旋而是采用分段累加最终汇总。退避策略在重试失败时短暂Thread.yield()或pause指令x86来减少 CPU 流水线冲突。4.3.3 只能保证一个共享变量的原子操作现象CAS 只能针对单个内存地址进行操作。解决方案将多个变量封装成一个对象使用AtomicReference进行 CAS。使用锁synchronized/Lock来处理多个变量的复合逻辑。5. 原子类Atomic源码剖析与实战5.1 基本类型原子类AtomicInteger以AtomicInteger为例核心源码分析javapublic class AtomicInteger extends Number implements java.io.Serializable { private static final Unsafe unsafe Unsafe.getUnsafe(); private static final long valueOffset; // 字段 value 的内存偏移量 static { try { valueOffset unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField(value)); } catch (Exception ex) { throw new Error(ex); } } private volatile int value; // 核心volatile 保证可见性 // 核心方法自旋CAS public final int getAndAdd(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta); } }unsafe.getAndAddInt的实现逻辑模拟java// 伪代码实际是 native 循环 public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v getIntVolatile(o, offset); // 获取当前值 } while (!compareAndSwapInt(o, offset, v, v delta)); return v; }5.1.1 性能分析低竞争CAS 直接更新效率极高。高竞争大量线程不断失败、重试导致总线流量风暴性能急剧下降甚至比synchronized差。5.2 数组类型原子类AtomicIntegerArray该类可以原子地更新数组中的元素。其核心在于通过baseOffset数组头地址和scale元素大小计算每个元素的内存地址。java// 计算第 i 个元素的偏移量 long offset baseOffset (long) i * scale; unsafe.compareAndSwapInt(array, offset, expected, update);5.3 引用类型原子类AtomicReference、AtomicStampedReference5.3.1 AtomicReference用于对对象引用进行原子更新常用于实现无锁栈、无锁队列。实战无锁栈 (Treiber Stack)javapublic class LockFreeStackT { private AtomicReferenceNodeT top new AtomicReference(); public void push(T value) { NodeT newHead new Node(value); NodeT oldHead; do { oldHead top.get(); newHead.next oldHead; } while (!top.compareAndSet(oldHead, newHead)); } public T pop() { NodeT oldHead; NodeT newHead; do { oldHead top.get(); if (oldHead null) return null; newHead oldHead.next; } while (!top.compareAndSet(oldHead, newHead)); return oldHead.value; } static class NodeT { final T value; NodeT next; Node(T value) { this.value value; } } }5.3.2 AtomicStampedReference解决 ABA相比AtomicReference它增加了stamp版本号。java// 初始化引用 版本号 AtomicStampedReferenceString ref new AtomicStampedReference(A, 0); // 修改必须同时匹配引用和版本号 boolean success ref.compareAndSet(A, B, 0, 1);5.4 字段更新器AtomicIntegerFieldUpdater作用将普通的volatile整数字段升级为支持原子操作无需修改原有类结构。适用于不想将整个类都改成原子类但又需要对特定字段进行原子操作的场景。约束字段必须是volatile的。字段不能被static修饰除非使用AtomicIntegerFieldUpdater的子类实现。字段对 Updater 必须是可见的通常用protected或public。示例javaclass Candidate { volatile int score; } AtomicIntegerFieldUpdaterCandidate updater AtomicIntegerFieldUpdater.newUpdater(Candidate.class, score); Candidate stu new Candidate(); updater.incrementAndGet(stu); // 原子增加6. 进阶从 CAS 到 LongAdder 的演进6.1 高并发下的 CAS 性能瓶颈在AtomicLong的高并发场景下所有线程都去争抢同一个 value 变量的更新权限导致大量的 CAS 失败和重试。这实际上是把并发竞争压力集中到了同一个热点变量上。6.2 Striped64 与分段累加思想LongAdder是AtomicLong在高并发场景下的替代品。它继承自Striped64核心思想是空间换时间 分段锁实际上是分散热点。数据结构base基础值在没有竞争时直接 CAS 更新base。Cell[]一个数组每个 Cell 是一个独立的计数器。当竞争激烈时线程会通过哈希算法映射到某个 Cell 上进行 CAS 操作将压力分散到多个 Cell 上。核心流程尝试 CAS 更新base。如果失败说明有竞争检查Cell[]是否初始化若未初始化则进行初始化。计算线程的哈希值定位到具体的Cell尝试 CAS 更新该 Cell。如果该 Cell 也竞争失败尝试扩容Cell[]数组扩大容量或重新哈希重新映射线程到其他 Cell。最终求和sum()方法会返回base 所有 Cell 的 value之和。性能对比低竞争AtomicLong和LongAdder性能相近。高竞争LongAdder的吞吐量远高于AtomicLong但代价是sum()方法不是强一致性的在求和过程中可能有新数据写入适用于统计型、准实时场景。7. 无锁并发框架的核心AQS 与 CAS 的协同AbstractQueuedSynchronizer (AQS)是 Java 并发包JUC的基石。虽然它内部使用了 CAS 来维护状态但本质上它是一个“锁框架”而非纯粹的无锁并发。AQS 巧妙地结合了 CAS 和阻塞同步状态state使用volatile int state配合 CAS 来原子地获取锁。compareAndSetState(0, 1)尝试获取锁。CLH 队列当 CAS 获取锁失败时线程不会自旋死循环而是被封装成 Node 节点通过 CAS 尾插法进入 FIFO 队列并利用LockSupport.park()进入阻塞状态释放 CPU。这种设计在“非竞争”状态下是无锁的CAS 快速获取在“竞争”状态下是阻塞的避免了无锁算法在高竞争下 CPU 空转的弊端。8. 实战无锁并发设计与性能调优8.1 使用 LongAdder 替代 AtomicLong 作为计数器场景接口请求计数器、QPS 统计。java// 不推荐高并发下性能差 private AtomicLong count new AtomicLong(0); count.incrementAndGet(); // 推荐高并发下性能优 private LongAdder count new LongAdder(); count.increment(); long total count.sum(); // 注意非强一致性8.2 无锁线程安全对象池使用 AtomicReference实现一个简单的无锁对象池例如数据库连接池的 Idle 资源管理利用AtomicReference和 CAS 避免锁竞争。8.3 消除伪共享False Sharing在多核 CPU 中缓存行Cache Line通常 64 字节是缓存交换的最小单位。如果两个无关的变量如两个线程各自的计数器位于同一个缓存行当线程 A 修改变量 X 时会刷新整个缓存行导致线程 B 的缓存行失效必须重新从内存加载变量 Y造成性能下降。解决方案填充Padding。在 Java 8 中可以使用Contended注解需要 JVM 参数-XX:-RestrictContended来让 JVM 自动将变量隔离到独立的缓存行。Striped64内部的Cell类就使用了Contended注解来防止伪共享。java// Striped64 源码片段 sun.misc.Contended static final class Cell { volatile long value; // ... }8.4 衡量标准权衡 CPU 与 延迟无锁/自旋 CAS适用于临界区极短、竞争不激烈的场景。如果临界区逻辑复杂自旋会浪费大量 CPU。锁适用于临界区较长、竞争激烈的场景因为阻塞会让出 CPU。9. 总结无锁并发的最佳实践原则能用volatile解决可见性问题就不要用 CAS能用 CAS 解决原子性问题就不要用锁synchronized/Lock。适用场景统计计数LongAdder优于AtomicLong。标志位volatile boolean足够。单一变量原子更新AtomicInteger/AtomicReference。避免 ABAAtomicStampedReference。性能陷阱高并发下的 CAS 自旋是 CPU 杀手考虑降级或使用LongAdder。注意伪共享尤其是在数组或高并发字段密集的场景。volatile不能替代锁它不能保证复合操作的原子性。底层认知CAS 依赖于硬件指令如 x86 的CMPXCHG比锁轻量。synchronized在 JVM 层面经过了大量优化偏向锁、轻量级锁、重量级锁在低竞争时也会使用 CAS轻量级锁因此并不是所有的锁都“重”。