这篇博客主要讲解两个问题:
1,Java当中CAS底层实现
2,CAS的ABA问题和解决办法
Java当中CAS底层实现
首先我们来看JDK1.7的AtomicInteger类的incrementAndGet源码:
private volatile int value;
public final int get() {
return value;
}
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
这段代码是一个无限循环,也就是CAS的自旋操作。循环体中做了3件事情:
1,获取当前变量的值;
2,对当前值做+1操作,得到目标值;
3,进行CAS操作,如果成功则跳出循环,失败则重复上述操作。
volatile保证了获取的当前值是内存中最新的值。
接下来看看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);
}
compareAndSet方法的实现很简单,只有一行代码,这里涉及到两个重要对象,一个是unsafe,一个是vauleOffset。
1,什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
2,至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们简单的把valueOffset理解为value变量的内存地址。
3,上一期我们提到了CAS机制使用的3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。而unsafe的compareAndSwapInt方法参数包括了这三个基本元素:valueOffset代表V,expect代表A,update代表B。正式unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
ABA问题
举个提款机的例子:小明有200元存款,想用提款机取100元
由于提款机硬件出了点问题,小明的提款操作被同时提交了2次,开启了两个线程,2个线程都是获取当前值200,要更新成100。理想情况下,应该一个线程更新成功,一个线程更新失败,小明的存款只被扣一次。
线程1首先执行成功,余额变为100,线程2由于某种原因被阻塞了,这个时候小明的妈妈刚好给小明汇款100元。
线程2仍然阻塞,线程3执行成功,把余额从100变成200
线程2恢复运行,由于阻塞之前获取的当前值是200,而此时内存地址中的值也是200,compare检测成功,执行更新操作变成了100。
原本线程2应该执行失败,小明的余额应该为200,但是由于ABA问题提交成功了。
如何解决ABA问题呢?解决办法就是加入版本号。
我们在Compare阶段不仅要比较期望值A和地址V中的值,我们还要比较变量的版本号是否一致。
举个例子:
假设地址V中存储着变量值A,当前版本号是01,线程1获取当前值A和版本号01,想要更新为B,但是被阻塞了。
这个时候,内存地址V中的变量发生了多次改变,版本号提升为03,但是变量值仍然是A。
线程1恢复运行后进行compare,泛型当前值为A,但是版本号为03与01不相等,所以这次更新失败。
总结:
1,Java语言CAS底层如何实现?
利用unsafe提供了原子性操作方法。
2,什么是ABA问题?怎么解决?
当一个值从A更新为B,又从B更新为A,不同的CAS机制会误判导致通过检测;利用版本号比较可以有效的解决ABA问题。