深入理解 CAS:并发编程的原子操作基石
前言:
在多线程并发编程中,保证数据操作的原子性是一个关键问题。传统的锁机制(如synchronized)虽然能够保证原子性,但会带来性能开销和线程阻塞等问题。为了解决这些问题,CAS作为一种乐观锁技术应运而生,凭借其无锁、高效的特性,成为了并发编程领域的重要基石。
一、什么是CAS?
CAS的全称是Compare AndSwap,中文意思是“比较并交换”。CAS 是一种乐观锁技术,它基于硬件原语(如 CPU 的cmpxchg
指令)实现,用于在多线程环境下对共享变量进行原子操作。
CAS 操作包含三个核心参数:
- 内存地址 V:要操作的共享变量在内存中的地址。
- 预期值 A:线程执行 CAS 操作前,认为该共享变量应有的值。
- 新值 B:如果共享变量的当前值与预期值 A 一致,线程希望将其更新为的新值。
CAS 的核心逻辑是:比较内存地址 V 中的值与预期值 A 是否相等,如果相等,就将该值更新为新值 B;如果不相等,说明有其他线程修改过该变量,当前线程不进行更新,但会获取变量的最新值,然后重新尝试 CAS 操作(通常是循环重试,即 “自旋”)。整个过程是原子的,不会被其他线程中断。
二、CAS执行流程
CAS操作的执行流程可以概括为”读取-计算-比较并交换“,具体步骤如下:
读取内存位置的当前值作为预期原值A
计算新值B
执行CAS操作,比较内存位置当前值是否仍等于A
a.如果相等,说明变量值未被其他线程修改,执行交换操作,将内存值更新为B;
b.如果不相等,说明变量值已被其他线程修改,操作失败。
4.重试机制:可以通过Java代码中的while循环再次调用cmpxchg指令进行重试,直到设置成功为止。这种"读取-计算-比较并交换"的循环模式就是著名的"自旋"。
三、Unsafe 类
在 Java 中,CAS 机制是通过 sun.misc.Unsafe
类提供的 compareAndSwapXXX()
等 CAS 方法来实现的。sun.misc.Unsafe
是 JDK 提供的一个底层工具类。它提供内存操作、CAS、对象操作等 “不安全” 的功能,让 JDK 能够使用 Java 代码来实现原本需要使用 native
本地方法(C 或 C++)才可以实现的功能。由于该类不应该在 JDK 核心类库之外使用,所以被命名为 Unsafe
(不安全)。
Unsafe类提供了 objectFieldOffset()
方法,用于获取某个字段相对 Java 对象的 “起始地址” 的偏移量(内存位置),还有 getInt()
、getLong()
、getObject()
等方法。可以按照字段偏移量,直接访问内存中 Java 对象的某个字段的内存地址。
四、Unsafe 实现 CAS 的工作原理
AtomicInteger类:
Unsafe类:
首先读取当前对象obj在主内存中的值,并保存到value中,然后通过循环,判断当前对象在主内存中的值是否等于value,如果相同,就自增(交换value与value+val两个值),否则继续循环,重新获取value值。
在上述逻辑中核心方法是compareAndSwapInt()方法,它是一个native本地方法,通过JNI(Java Native Interface)调用底层C++代码,执行cPU指令是cmpxchg。
在getAndAddInt()方法中通过do...while循环操作实现自旋锁:当预期值和主内存中的值不等时,就重新获取主内存中的值。
五、基于 CAS 的 AtomicInteger
AtomicInteger
是JUC包中基于CAS实现的原子整数类,它提供了一系列原子操作的方法。
public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID = 6214790243416807050L;// 获取Unsafe实例private static final Unsafe unsafe = Unsafe.getUnsafe();// 存储value字段的内存偏移地址private static final long valueOffset;static {try {// 获取value字段的偏移地址valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}// 当前值,使用volatile保证可见性private volatile int value;// CAS更新操作public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);}// 原子递增public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;}// 其他原子方法... }
六、 CAS的缺点
1.循环时间长开销大
在Unsafe的实现中使用了自旋锁的机制。在该环节如果CAS操作失败,就需要循环进行CAS操作(do...while循环同时读取最新的期望值),如果长时间都不成功的话,那么会造成CPU极大的开销。
2.只能保证一个共享变量的原子操作
CAS操作只能保证一个共享变量的原子性,但如果存在多个共享变量,或一整个代码块的逻辑需要保证线程安全,CAS就无法保证原子性操作了。此时,就需要考虑采用加锁方式(悲观锁)保证原子性。
3.ABA问题
ABA问题是CAS机制中的一个经典问题:如果一个变量的值从A变为B,然后又变回A,那么CAS操作会误认为它没有被修改过。
例如:
线程1读取变量值为A
线程2将值从A改为B
线程3又将值从B改回A
线程1执行CAS操作,发现值仍是A,认为没有被修改过,操作成功
虽然大多数情况下这不是问题,但在某些场景下(如链表操作)可能导致错误。
解决方案:可以通过JDK的Atomic包中的AtomicStampedReference类来解决,使用compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
七、结语
CAS作为一种高效的无锁并发机制,在Java并发编程中发挥着重要作用。它通过硬件级别的原子指令实现了线程安全,避免了传统锁机制的性能开销。然而,CAS也存在一些局限性,需要根据具体场景选择合适的使用方式。理解CAS的原理和实现对于编写高性能并发程序至关重要。