CAS(Compare and Swap)(比较与交换)
CAS是保障而Atomic操作类的底层实现的核心机制
什么是Atomic操作类?
指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。
如:AtomicBoolean,AtomicInteger,AtomicLong。
它们分别用于Boolean,Integer,Long类型的原子性操作。
CAS机制中使用了3个基本操作数:
1:内存地址V
2:旧的预期值A
3:要修改的新值B
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
1:在内存地址V当中,存储着值为10的变量。
2: 此时线程1想把变量的值增加1.
对线程1来说,旧的预期值A=10,要修改成 新值B=11.
3:在线程1要提交更新之前:
另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4:线程1开始提交更新:
首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。
5: 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。
此时对线程1来说,A=11,B=12。
这个重新尝试的过程被称为自旋。
6:这一次比较幸运,没有其他线程改变地址V的值。
线程1进行比较,发现A和地址V的实际值是相等的。
7:线程1进行交换,把地址V的值替换为B,也就是12.
CAS的缺点:
1:CPU开销过大在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,
循环往复,一直自旋,会给CPU带来很到的压力。
2:不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。
比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
3:ABA(CAS机制最大的问题)(后续介绍)
CAS的底层是怎么实现的
//AtomicInteger当中常用的自增方法incrementAndGet
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
private volatile int value;
public final int get() {
return value;
}
这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:
1. 获取当前值
2. 当前值+1,计算出目标值
3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤
//compareAndSet实现 的源码分析
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。
//Java语言不像C,C++那样可以直接访问底层操作系统,
//但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
//至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到的,
//所代表的是AtomicInteger对象value成员变量在内存中的偏移量。
//我们可以简单的把valueOffset理解为value变量的内存地址。
ABA(CAS机制会误判通过检测)
ABA所产生的实际问题:
我有100块钱存款,现在要用提款机来提50元
由于机器故障,提款的操作被同时提交了两次
线程1:获取当前值100 期望更新为50
线程2:获取当前值100 期望更新为50
线程1:首先执行成功 余额从100改成50
线程2:由于某种原因阻塞
线程3:别人在这个时候给我汇入了50
线程3:执行成功
线程2:现在刚好不阻塞了。。。我现在剩下50元了
如何解决ABA问题:
设置版本号
当内存地址V中变量发生了改变后,版本号进行提升;
线程1:获取当前值100 版本号01 期望更新为50
线程2:获取当前值100 版本号01 期望更新为50
线程1:首先执行成功 余额从100改成50 版本号02
线程2:由于某种原因阻塞
线程3:别人在这个时候给我汇入了50
线程3:执行成功 版本号03
线程2:现在刚好不阻塞了。。。但版本号对不上 不更新