CAS机制

前言

在jdk 5 之前,在多线程编程的时候,为了保证多个线程对一个对象同时进行访问,我们需要加同步锁synchronized,保证对象在使用时的正确性,但是加锁机制会导致如下几个问题:

  • 在多线程竞争下,加锁和释放锁会导致较多的上下文切换,引起性能问题
  • 多线程可以导致死锁的问题
  • 多线程持有的锁会导致其他需要此锁的线程挂起
  • 如果一个优先级较高的线程等待一个优先级较低的线程释放锁会导致优先级倒置,引起性能风险

volative不能保证原子性,因此同步还是需要用到锁。

锁的分类:独占锁(悲观锁),乐观锁

独占锁:synchronized就是一种独占锁,它会导致所有需要此锁的线程挂起,等待锁的释放

乐观锁:每次不加锁去完成操作,如果因为冲突失败就重试,知道成功,本质上不算锁,所以很多地方也称之为自旋,乐观锁用到的主要机制就是CAS(compare and swap)

CAS机制就相当于这种(非阻塞算法),CAS是由CPU硬件实现,所以执行相当块。CAS有三个操作参数,内存地址,期望值,要修改的新值,当期望值和内存当中的值进行比较不相等的时候,表示内存中的值已经被别的线程改动过,这时候失败返回,当相等的时候,将内存中的值改为新的值,并返回成功。

举个栗子——AtomicInteger

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
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
private static final long VALUE;
static {
try {
VALUE = U.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (ReflectiveOperationException e) {
throw new Error(e);
}
}
private volatile int value;
...
public final int getAndAdd(int delta) {
return U.getAndAddInt(this, VALUE, delta);
}
public final int incrementAndGet() {
return U.getAndAddInt(this, VALUE, 1) + 1;
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final class Unsafe {
...
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
...
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
...
}

这里显然采用了CAS机制,每次从内存中读取数据都需要和+1后的数据进行一次CAS操作,如果成功返回结果,否则就失败重试,知道重试成功为止。其中,compareAndSwapInt是利用JNI来完成CPU指令的操作。

小结

利用CPU的CAS机制,同时借助JNI来完成Java的非阻塞算法,基本上Java中的原子类都是使用类似的机制来保证数据对策原子操作的。

尽管CAS机制使得我们可以不依赖同步,不影响和挂起线程实现原子性操作,能大大提升运行时的性能,但是会导致一个ABA的问题。如线程一和线程二都取出了主存中的数据为A,这时候线程二将数据修改为B,然后又修改为A,此时线程一再次去进行compareAndSwapInt的时候仍然能够匹配成功,而实际对数据已经发生了变更,只不过发生了两次将对应的值修改为原始的数据了,并不代表实际数据没有发生变化。