初识CAS

对于一个java小白来说,聊到并发问题,为了保证数据一致性,我首先会想到加锁—— java 中的 Synchronized 关键字。但是这个想法被大神严格否定了,因为 Synchronized 是一种悲观锁,存在一定的性能问题。

锁机制

java中的锁机制是为了实现数据一致的一种方式,在并发的情况下,共享资源可能同时被多个线程使用。假设 A 线程和 B 线程同时获取到变量 i,并对其加 1 ,那么最终放回内存的值,很可能是 i+1 而不是正确的 i+2。所以,我们需要对这部分操作加锁,同一时间仅允许一个线程对资源进行操作。

悲观锁

从字面上意思来看,悲观地认为一定会与其他线程产生冲突,那么首先将资源加锁,只能自己进行操作,操作完成之后释放锁。悲观锁会带来一定的性能问题,由于cpu中不停地进行线程调度,当一个线程获取不到资源,会被挂起,让出cpu,等到资源可用时才被唤醒,这个过程存在很大的开销。

乐观锁

乐观地认为不会与其他线程产生冲突,首先获取资源,但是在执行修改时,会先判断有没有被其余线程更改过。如果发生冲突,则不对资源进行修改,同时不让出cpu,不断进行重试,直到修改成功。

CAS简述

CAS即比较并交换(Compare And Swap),是一种乐观锁的实现。CAS概念中有3个参数:

  • 内存地址 V
  • 最后一次获取到的内存值 A (old value)
  • 待写入内存的值 B (new value)

CAS认为,地址 V 中的值应当与 A 一致,如果确认一致,那么写入在 V 中写入 B ,反之,不执行写入操作,并认为操作失败。

Unsafe类

java 中的 CAS 操作使用 jdk 提供的 Unsafe 类,在 rt.jar 中。java 不能直接访问底层操作系统,需要通过 native 方法来访问。Unsafe 类包含了许多硬件级别的原子操作,包括对内存的读写等。这些操作主要用于 java 的核心类库使用,而不是标准用户。Unsafe 提供了三个方法实现 CAS 操作,分别用于三种不同的数据类型:

1
2
3
4
5
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

我们可以使用这些方法,实现一个基于 CAS 的计数器。

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
27
28
29
30
31
32
33
class CASCounter {
private Unsafe unsafe;
private volatile long counter = 0;//counter声明为volatile类型,多个线程共享
private long offset;

/**
* 通过反射获取 Unsafe 对象
* @return
* @throws IllegalAccessException
* @throws NoSuchFieldException
*/
private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}

public CASCounter() throws Exception {
unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
}

public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}

public long getCounter() {
return counter;
}
}

代码中,首先使用反射获取到 Unsafe 对象(直接使用 Unsafe.getUnsafe() 会抛出 SecurityException 异常)。在CASCounter 的构造方法中,传入 Unsafe 对象与 counter 字段的内存地址。执行增加操作时,首先将 counter 赋予 before,再通过 compareAndSwapLong 方法,比较 offset 地址的值是否与我们所获取的一致,不一致则返回 false,并更新 before 值。

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
27
28
29
@Test
public void test() throws Exception {
int NUM_OF_THREADS = 5;
int NUM_OF_INCREMENTS = 5;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
CASCounter casCounter = new CASCounter();

List<Future> futures = new ArrayList<>();

IntStream.rangeClosed(0, NUM_OF_THREADS - 1)
.forEach(i -> {
Future future = service.submit(() -> IntStream
.rangeClosed(0, NUM_OF_INCREMENTS - 1)
.forEach(j -> {
casCounter.increment();
}));
futures.add(future);
});

futures.forEach(future -> {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});

assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());
}

测试代码中,启动 5 个线程,每个线程对计数器执行 5 次操作。使用 Future 等待线程执行完毕。断言计数器执行的次数与我们预期的次数相等。

JAVA 源码中的应用

java.concurrent 包中的许多方法,都是基于 Unsafe 类的 compareAndSwap

iKa7dI.md.png

比如 ConcurrentHashMap 中的 addCount 方法等(只展示了部分源码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if ···
}

由于 CAS 的操作能够避免线程阻塞,在编码中的应用十分常见。同时,对于这种思维的学习,也有利于理解更多核心代码。

CAS 缺点

学习的过程中,了解到一些 CAS 的缺点,不过可能在真正应用的时候才能有深刻理解吧。

  • ABA 问题。即一个值从 A 变为 B 再变为 A,而 CAS 操作不能发现这个值发生了变化。解决思路可以引入版本号概念,每一次修改都设置版本号。
  • 只能操作一个共享变量。根据提供的本地方法可以看到,使用 CAS 一次只能操作一个共享变量。
  • 循环时间所带来的开销。由于 CAS 会不断重试直到操作成功,过程中无限循环,如果线程数众多,并且长时间执行不成功,会给 CPU 带来大量的执行开销。

参考文档

Java Compare and Swap Example – CAS Algorithm

Guide to sun.misc.Unsafe

Java CAS 理解

JAVA CAS原理深度分析