0%

CAS操作

CAS操作:Java 并发的无锁基石

CAS(Compare And Swap)是一种无锁算法,用于实现多线程环境下的原子操作。它通过比较内存中的实际值与预期值,仅在两者相等时才进行更新,从而避免了传统锁机制的开销。这种思想是 Java 并发包(java.util.concurrent)的核心之一,尤其在atomic包的原子类中广泛应用。

CAS 的核心原理与实现

CAS 操作涉及三个关键参数:

  • V(内存地址):要操作的变量在内存中的位置;
  • A(预期值):线程认为 V 处当前应该的值;
  • B(新值):若 V 处的值等于 A,则将 V 更新为 B。

整个过程由 CPU 的原子指令(如 x86 的CMPXCHG)保证原子性,无需加锁。

AtomicInteger 中的 CAS 实现

AtomicInteger为例,其compareAndSet方法的底层实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AtomicInteger {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value; // 用volatile保证可见性

static {
try {
// 获取value字段在内存中的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

// 核心CAS方法
public final boolean compareAndSet(int expect, int update) {
// 调用Unsafe的本地方法实现CAS
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}

Unsafe 类:CAS 的底层支撑

Unsafe是 JDK 提供的一个强大但危险的工具类,通过它可以直接操作内存和线程。其核心方法包括:

方法 作用
objectFieldOffset(Field) 获取字段在对象中的内存偏移量
compareAndSwapInt(...) 原子性地比较并替换 int 值
park(boolean, long) 阻塞当前线程(可指定超时)
unpark(Object) 唤醒指定线程

如何使用 Unsafe?

由于安全限制,直接调用Unsafe.getUnsafe()会抛出异常。通常通过反射获取实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TestUnsafe {
private static final Unsafe unsafe;
private static final long valueOffset;
private volatile int value = 0;

static {
try {
// 通过反射获取Unsafe实例
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe = (Unsafe) theUnsafe.get(null);

// 获取value字段的偏移量
valueOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("value"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) {
TestUnsafe test = new TestUnsafe();
// 原子性地增加value值
unsafe.getAndAddInt(test, valueOffset, 1);
System.out.println(test.value); // 输出1
}
}

CAS 的三大核心缺陷

尽管 CAS 高效,但存在以下局限性:

CPU 开销大

在高并发场景下,大量线程频繁尝试 CAS 操作却失败,会导致自旋(空循环),消耗 CPU 资源。

解决方案

  • 降低竞争:通过数据分片(如LongAdder)分散热点;
  • 结合锁:竞争激烈时使用synchronizedReentrantLock

只能保证单个变量的原子性

CAS 仅能对单个变量进行原子操作,无法扩展到多个变量或代码块。

解决方案

  • 使用AtomicReference封装多个变量;
  • 使用锁机制(如synchronized)保证代码块原子性。

ABA 问题

问题描述:变量值从 A 变为 B,再变回 A,CAS 认为值未变化,但实际中间发生了修改。

示例场景

1
2
3
// 线程1:期望将100改为200
// 线程2:先将100改为150,再改回100
// 线程1的CAS操作仍会成功,但忽略了中间的变化

解决方案:使用AtomicStampedReferenceAtomicMarkableReference,为变量增加版本号或标记位:

1
2
3
4
5
6
7
AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);

// 线程1:更新时需同时验证值和版本号
int stamp = ref.getStamp();
boolean success = ref.compareAndSet(100, 200, stamp, stamp + 1);

// 线程2:即使值改回100,版本号已变化,线程1的CAS失败

CAS 在 JDK 中的典型应用

1. Atomic 系列类

AtomicIntegerAtomicLongAtomicBoolean等通过 CAS 实现原子操作,替代synchronized

2. 并发容器

ConcurrentHashMap在 JDK 8 + 中使用 CAS 替代分段锁,提升并发性能。

3. AQS(AbstractQueuedSynchronizer)

ReentrantLockSemaphore等基于 AQS 实现,其核心状态变量通过 CAS 更新。

欢迎关注我的其它发布渠道