在java中锁可以分为悲观锁和乐观锁,今天介绍的CAS就是属于乐观锁的一种实现方式。CAS全称为 Compare And Swap 即比较和交换。乐观锁的乐观的意思就是我认为自己肯定会拿到锁资源,如果拿不到我就一直尝试,直到拿到锁资源为止。
接下来以AtomicInteger原子类来详细解释说明:
private static int threadNum = 100;
private static int numberA = 0;
private static AtomicInteger numberB = new AtomicInteger(0);
private static final ExecutorService executorService = Executors.newCachedThreadPool();
public static void main(String[] args) {
for (int i = 0; i < threadNum; i++) {
executorService.execute(() -> {
numberA += 1;
numberB.getAndIncrement();
});
}
System.out.println(numberA); // 95
System.out.println(numberB); // 100
}
通过上面代码可以看出在多线程场景下对AtomicInteger操作是线程安全的而对numberA变量的操作是线程不安全的。
public final int getAndIncrement() {
// 使用unsafe对象的getAndAddInt方法
return unsafe.getAndAddInt(this, valueOffset, 1);
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 获取value字段相对于当前对象的的offset(起始地址)
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
这里的静态代码块AtomicInteger对象初始化之前就执行了,获取AtomicInteger对象value字段相对AtomicInteger对象的”起始地址”的偏移量,Java对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),”起始地址”的偏移量即是对象头的偏移量。
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;
}
于上述代码var1,var2等变量不见名知意,转化如下:
public final int getAndAddInt(Object obj, long valueOffset, int delta) {
int expect;
do {
// 先获取内存offset出的value值
expect = this.getIntVolatile(obj, valueOffset);
// 然后通过compareAndSwapInt比较并且交换
// delta为当前value需要加上的数值
// update = expect + delta
} while(!this.compareAndSwapInt(obj, valueOffset expect, update));
return expect;
}
上述代码流程为:
其中比较和修改要保证原子性
CAS底层靠调用CPU指令集cmpxchg来完成原子操作的。但在多核场景下该指令也不能保证原子性。需要在前面加上lock指令。lock指令可以保证一个CPU核心在操作期间独占一片内存区域。在多核处理中一般通过总线锁和缓存锁来实现。
通过上面的AtomicInteger源码的阅读,我对CAS的理解:
CAS操作是不加锁的, 它每次都假设自己可以执行成功, 然后它就去尝试执行了,成功就返回,否则就一直循环去不断地尝试。这样做比加锁和释放锁的性能是要高的。但是如果一直都是失败的,这样长时间的循环(自旋)会给CPU带来很大的开销。
其次CAS还会有ABA的问题,比如我原来value的值为 A, 期间改成了B,后来有变成了A,这个时候某一个线程去执行CAS的之后就会认为value的值没有更改过。但是实际却变化过了。可以使用AtomicStampedReference类解决ABA问题。
还有就是CAS只能保证一个共享变量的原子操作, 没办法保证一块代码或者一个对象的原子操作。