多线程—乐观锁CAS机制

宋正兵 on 2021-03-22

参考:【基本功】不可不说的Java“锁”事

悲观锁

多个线程尝试获取同步资源的锁(又或者说是给同步资源加锁)

  • 加锁成功:操作资源,当执行完毕后,释放锁,然后 CPU 会唤醒等待的线程再次尝试获取锁
  • 加锁失败:进入等待队列,当 CPU 唤醒后再次尝试获取锁

乐观锁

线程直接获取同步资源数据,执行各自的操作,但是在更新内存中的同步资源之前先判断资源是否被其他线程修改,据此进行不同的操作:

  • 同步资源没有被更新:更新内存中同步资源的值
  • 同步资源被其他线程更新:根据实现方法执行不同的操作(报错 or 重试)

meituan

什么是CAS?

CAS(Compare And Swap),即比较并交换,它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的。

当多个线程尝试使用 CAS 同时更新一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程不会被挂起,而是被告知在这次竞争中失败,并且可以再次尝试。

CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B,否则处理器不做任何操作。CAS 是通过无限循环来获取数据的,若果在第一轮循环中,t1 线程获取地址里面的值被 t2 线程修改了,那么 t1 线程需要自旋,到下次循环才有可能机会执行。

1
2
3
4
5
6
7
8
9
public void add(int num) {
while (true) {
int prev = ref.get();
int next = pre + num;
if (ref.compareAndSet(prev, next)) {
break;
}
}
}

CAS缺点

  1. 循环时间长,开销大。如果CAS失败,会一直进行尝试.如果CAS长时间一直不成功,可能会给CPU带来很大的开销
  2. 只能保证一个共享变量的原子性
  3. 产生ABA问题

ABA问题

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
participant t1
participant Memory
participant t2

Memory ->> t1: 获取值为A
Memory ->> t2: 获取值为A
t2 -->> Memory: compareAndSet(A,B),修改为B
Memory ->> t2: 获取值为B
t2 -->> Memory: compareAndSet(B,A),修改为A
t1 -->> Memory: compareAndSet(A,C),修改为C

如上图所示,在线程 t1 从内存中取出变量值 A,这时另外一个线程 t2 也从内存中取出变量值 A,并且线程 t2 进行了一些操作将内存中变量值变成了 B,然后线程 t2 又将内存中变量值变成了 A,这时候线程 t1 进行 CAS 操作发现内存中仍然是 A,然后线程 t1 操作成功。

尽管线程 t1 的 CAS 操作是成功的,但是不代表这个过程就是没有问题的。

解决方法:

  1. 使用 AtomicStampedReference 类,可以记录更新操作的时间戳版本号;
  2. 使用 AtomicMarkableRenference 类,根据标志位来判断是否更换过。