上一篇讲到,jdk1.5引入了并发包,java.util.concurrent.atomic包下有很多原子变量的操作类,但是基本都存在一个问题,ABA问题,也就是并发场景下原子变量被修改了,然后又被改回来,线程再做CAS计算的时候无法感知当前的原子变量的内容已经不是当初的内容了,只是单纯的比较值是否相等无法满足一些特定的业务场景。当然Doug Lea大神也考虑到了这个问题,于是就引入了AtomicStampedReference和AtomicMarkableReference,接下来就AtomicStampedReference的原理和用法做一下详细的介绍。
首先还是先看一下AtomicStampedReference的声明和代码结构:
类声明中定义了一个泛型V,也就是AtomicStampedReference包裹的具体内容,然后接着定义了一个私有静态内部类Pair,同样有声明泛型,有两个成员变量,reference是引用的值,stamp是版本号也可以理解为时间戳(尽量满足单向递增),Pair类的意思就是存储一个值和版本号对应的关系,便于修改的时候作比较。然后可以看到一个成员变量pair,和唯一的构造器,构造器必须有两个入参,对应pair中的两个变量。
然后接着看代码,我们找到了如下一段代码:
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
和上一篇相似,也是声明内存操作变量UNSAFE和获取成员变量pair的相对位置。
然后我们找到一个更新操作的方法compareAndSet,没猜错的话能够解决原子变量ABA问题的就是它了,看一下代码实现:
/**
* Atomically sets the value of both the reference and stamp
* to the given update values if the
* current reference is {@code ==} to the expected reference
* and the current stamp is equal to the expected stamp.
*
* @param expectedReference the expected value of the reference
* @param newReference the new value for the reference
* @param expectedStamp the expected value of the stamp
* @param newStamp the new value for the stamp
* @return true if successful
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
从方法注释中我们看到,使用新的值和版本号原子性的修改原来的值和版本号。方法入参expectedReference表示期望内存中的值,newReference表示想修改成的值,期望当前值对应的版本号,newStamp表示修改成功后版本号改为的值;然后获当前对象中的变量pair,返回是否修改成功;expectedReference == current.reference表示要满足我拿到的值和pair中的值一致(未被其他线程修改成其他值),expectedStamp == current.stamp表示同时也要满足拿到的版本号和pair中的版本号一致,(newReference == current.reference && newStamp == current.stamp)表示要修改成的值和要修改成的版本号和当前pair中的值和版本号一致,也就是说没有做任何修改,返回成功,|| casPair(current, Pair.of(newReference, newStamp)) 表示尝试修改成新的值和版本号并返回操作结果;看下casPair的实现:
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
可以看到casPair是调用原生的CAS方法尝试将当前对象相对位置偏移量为pairOffset的值从cmp改成val,如果成功返回true,否则返回false。
接下来我们根据案例来分析其使用方式来更好的加深对AtomicStampedReference的理解:
我们新建了一个原子引用,初始值是10,初始版本号是0,然后尝试把引用的值从10改成11,将版本号从0改成1,根据打印结果可以看到修改成功,并且修改后的值和版本号符合我们的预期;然后我们把上述代码做一下修改:
把asr.compareAndSet第一个参数改成了9,根据打印结果我可以看到修改失败,并且值没有被修改。为什么修改失败,因为入参9表示期望当前的值,而实际是10,程序认为pair中的值已经被其他线程修改,此次修改失败。
然后将上述程序再做一下修改如下:
把期望的版本号从0改成了2,运行程序发现修改失败且内容未被修改。为什么呢?因为虽然期望的值一致,但是版本号不一致,也就是我们开篇所说的ABA问题,值被其他线程从10改成其他值又被改了回来,虽然值看起来没变,但是已经增加了两个版本号,这时候程序认为内容发生了变化,不允许修改。
经过上述的介绍,相信之前对原子变量ABA问题不是很理解的或者对原子变量引用AtomicStampedReference不太了解的,会有一个比较明了的认识和掌握。希望在实际开发中给大家带来帮助和源码层面实现思路的理解。如果有觉得分析不到位或者理解有偏差的,可以直接留言或者私聊我。
原创不易,请多多支持!
附带公众号:
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!