相信我们都知道乐观锁的底层是利用了CAS机制实现(如有不懂,请看上篇文章)你真的了解乐观锁、悲观锁吗?
Java的CAS底层实现
我们先来看看cnt.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;
}
这里需要注意一下这个get方法,为何要在声明value时候使用volatile关键字呢? 那是因为volatile关键字保证变量的可见性!保证获取当前值是内存中的最新值
而这段代码也是是ABA产生的原因
在以上代码中,可以看到compareAndSet(int expect, intupdate)的第1个参数,传进去的并不是版本号,而是数据的旧值。也就是说,它认为,只要数据的旧值expect = 数据当前的值,则说明在此期间没有其他线程修改过此数据,则把数据修改为新值update。
这种比较值,而不是比较版本号的做法,会产生经典的ABA问题。而这,也正是AtomicStampedReference要解决的。
public final boolean compareAndSet(int expect,int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
//value成员变量在内存中的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
safe?compareAndSwapInt方法的具体含义,以及这几个参数所表示的意义。
到底什么是unsafe?java不像c/c++可以直接访问底层操作系统,但是JVM却留了一手,unsafe可以为我们提供硬件级别的操作。
compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
CAS机制中使用了3个操作数:需要读写的内存值 V,进行比较的值 A,拟写入的新值 B,而unsafe的compareAndSwapInt方法参数包括三个元素,valueOffset参数代表了V,expect参数代表了A,update参数代表了B。
ABA问题
在线程1改数据期间,线程2把数据改为A,再改为B,再改回到A。这个时候,线程1做CAS的时候,如果只是比较值,则它会认为数据在此期间没有被改动过,而实际上数据已被线程2改动过3次。
我们举一个例子
假设银行有一个遵循CAS原理的提款机,小明有100元存款,现在需要取出50元。
由于取款机出现了点小问题,取款操作被提交了两次,开启了两个线程,两个线程都是获取当前余额100元,更新成50元。
理论上一个线程成功了,另一个线程失败了,余额只被扣了一次,余额为50.
但是这时候,线程1执行成功了,线程2因为部分原因阻塞,这时候小明妈妈往小明账户中汇款50元,这时候账户余额为100元,此时线程2恢复运行,因为阻塞之前获取的账户金额为100元,此时账户金额也为100元,线程2认为一致执行成功,余额会被更新为50元。而正确余额应该是100元。
这就是1A-2B-3A问题
那该怎么解决这个问题呢?其实逻辑也很简单,我们只需要加个版本号就行了,在Compare期间不仅要比较A和V中的值,还需要比较版本号是否一致。下面我们看看加入版本号的实现
//旧值,新值,旧版本号,新版本号
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
{
ReferenceIntegerPair<V> current = atomicRef.get();
return expectedReference ==current.reference &&
expectedStamp == current.integer &&
((newReference == current.reference &&
newStamp == current.integer) ||
atomicRef.compareAndSet(current,
newReferenceIntegerPair<V>(newReference,
newStamp)));
}
private static class ReferenceIntegerPair<T> {
private final T reference; //值
private final int integer; //版本号
ReferenceIntegerPair(T r, int i) {
reference = r; integer = i;
}
}
上面的atomicRef.compareAndSet(…)的第一个参数,传入的是一个ReferenceIntegerPair对象,它里面包含了2个字段:值 + 版本号。这也就意味着,它同时比较了值和版本号。
– 值不等,则肯定被其他线程改过了,不用再比较版本号,cas提交失败;
值相等,再比较版本号,如果版本号也相等,则说明真的没有被改过,cas提交成功;
值相等,版本号不等,则就是出现了ABA,CAS提交失败。
----------------END----------------
喜欢本文的朋友,欢迎关注公众号【程序员阿宝】,查看更多精彩内容
网友评论