Java CAS - 详解
Java 中的 CAS(Compare-And-Swap)。这是一个非常重要的概念,是 Java 并发编程中无锁(Lock-Free)算法的核心。
一、什么是 CAS?
CAS 的全称是 Compare-And-Swap,即比较并交换。它是一种原子操作,用于实现多线程环境下的同步,而不需要使用重量级的锁(如 synchronized
)。
它的操作过程包含三个操作数:
内存位置(V)
预期原值(A)
新值(B)
CAS 的原理:
当且仅当内存位置 V 的值等于预期原值 A 时,处理器才会将该位置的值更新为新值 B。否则,处理器不做任何操作。整个比较和交换操作是一个原子操作,执行期间不会被其他线程中断。
无论操作是否成功,它都会返回该内存位置的当前值。程序员可以根据返回的值来判断 CAS 操作是否成功。
二、在 Java 中如何实现 CAS?
Java 并没有直接提供 CAS 操作给程序员使用,而是通过 sun.misc.Unsafe
类中的一系列 compareAndSwap
本地方法(JNI)来底层实现的。
然而,我们通常不会直接使用 Unsafe
类(因为它被设计为仅供 Java 核心类库使用,是不安全的)。相反,JDK 在 java.util.concurrent.atomic
包下提供了一系列原子类(如 AtomicInteger
, AtomicLong
, AtomicReference
等),这些类为我们封装了 CAS 操作,提供了简单易用的 API。
一个经典的例子:使用 AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;public class CASDemo {public static void main(String[] args) {AtomicInteger atomicInt = new AtomicInteger(5); // 初始值为 5// 尝试进行 CAS 操作// 参数1:expect - 期望当前值是多少// 参数2:update - 如果期望值正确,要设置的新值是多少boolean success1 = atomicInt.compareAndSet(5, 10); // 当前值是5,匹配期望值5,所以更新为10,返回trueSystem.out.println("CAS操作1是否成功: " + success1 + ", 当前值: " + atomicInt.get());boolean success2 = atomicInt.compareAndSet(5, 15); // 当前值是10,不等于期望值5,所以更新失败,返回falseSystem.out.println("CAS操作2是否成功: " + success2 + ", 当前值: " + atomicInt.get()); // 值仍然是10}
}
输出:
CAS操作1是否成功: true, 当前值: 10
CAS操作2是否成功: false, 当前值: 10
compareAndSet
方法就是最典型的 CAS 操作在 Java 中的体现。
三、CAS 的底层原理
CAS 的原子性实际上是由硬件(主要是 CPU)来保障的。大部分现代处理器(如 x86)都提供了实现 CAS 操作的指令(例如 CMPXCHG
指令)。
Java 通过 JNI(Java Native Interface)调用这些由 C++ 实现的本地方法,这些本地方法再调用 CPU 的原子指令来完成操作。这个过程大致如下:
Java 代码:调用
AtomicInteger.compareAndSet()
。JVM: JVM 会将其转换为对
Unsafe.compareAndSwapInt()
方法的调用。本地方法:
Unsafe.compareAndSwapInt()
是一个native
方法,它通过 JNI 进入 JVM 的本地代码(C++)实现。汇编指令: 在本地代码中,会根据当前操作系统和 CPU 架构,调用对应的底层原子指令(如
lock cmpxchg
)。CPU 执行: CPU 原子性地执行比较和交换操作,并返回结果。
关键点: lock
前缀指令在多核处理器下确保了对内存操作的独占性。它会锁定一个小的内存区域或者总线,防止其他 CPU 在该指令执行期间访问该内存,从而保证了原子性。
四、CAS 的典型应用场景
原子类:
java.util.concurrent.atomic
包下的所有类,其自增、自减、累加等操作都是基于 CAS 实现的。AtomicInteger i = new AtomicInteger(0); i.incrementAndGet(); // 底层使用 CAS 循环实现,线程安全
并发容器: 如
ConcurrentHashMap
,在 JDK 1.8 后的实现中,大量使用了 CAS 来优化同步性能,比如在 putVal 方法中初始化数组桶、插入链表头节点等。锁机制: 许多显式锁(如
ReentrantLock
)和同步工具(如AQS
AbstractQueuedSynchronizer)的内部实现中,对状态的修改也大量使用了 CAS 来避免直接的锁竞争。
五、CAS 的优缺点
优点:
性能高: 它是一种乐观锁,在竞争不激烈的情况下,性能远高于传统的同步锁(如
synchronized
),因为它避免了线程阻塞和上下文切换的开销。避免死锁: 由于是无锁操作,从根本上避免了死锁问题。
缺点:
ABA 问题:
描述: 假设一个变量初始值为 A。线程1准备将其改为 C,于是先读取到值为 A。在此期间,线程2将值改为了 B,然后又改回了 A。这时线程1执行 CAS 操作,发现值还是 A,于是成功更新为 C。但这个过程对于线程1来说是感知不到中间状态 B 的,这就像什么都没发生一样,但在某些业务场景下这可能是个问题(例如一个链表的头节点被修改又改回,其内部状态可能已变化)。
解决方案: JDK 提供了
AtomicStampedReference
和AtomicMarkableReference
类,它们通过一个版本号(Stamp)或标记来避免 ABA 问题。每次修改版本号都会增加,CAS 操作同时比较值和版本号。
循环时间长开销大:
描述: 如果 CAS 操作长时间不成功(比如竞争非常激烈),CPU 会一直进行循环尝试,会给 CPU 带来很大的开销。
解决方案: 通常可以限制自旋次数,或者与更高级的锁机制结合使用。
只能保证一个共享变量的原子操作:
描述: 一个 CAS 操作只能针对一个变量。
解决方案: 如果需要同时对多个变量进行原子操作,可以将多个变量封装成一个对象,然后使用
AtomicReference
来保证原子性。JDK 也提供了AtomicReferenceFieldUpdater
等工具。
总结
特性 | 描述 |
---|---|
本质 | 一种硬件支持的原子操作指令(Compare-And-Swap)。 |
Java 体现 | 通过 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger )的 compareAndSet() 等方法暴露给开发者。 |
核心思想 | 乐观锁:先尝试修改,如果失败(发生冲突)就重试,而不是直接加锁。 |
优点 | 高性能(无阻塞,无上下文切换),避免死锁。 |
缺点 | ABA 问题(可通过版本号解决)、循环开销、只能操作一个变量。 |
CAS 是构建高效、高并发 Java 应用程序的基石之一,理解它对于深入掌握 Java 并发编程至关重要。