欢迎大家来到IT世界,在知识的湖畔探索吧!
难度
初级
学习时间
30分钟
适合人群
零基础
开发语言
Java
开发环境
- JDK v11
- IntelliJIDEA v2018.3
友情提示
- 本教学属于系列教学,内容具有连贯性,本章使用到的内容之前教学中都有详细讲解。
- 本章内容针对零基础或基础较差的同学比较友好,可能对于有基础的同学来说很简单,希望大家可以根据自己的实际情况选择继续看完或等待看下一篇文章。谢谢大家的谅解!
1.温故知新
前面在《“全栈2019”Java原子操作第七章:AtomicLong介绍与使用》一章中介绍了什么是原子操作类AtomicLong。
在《“全栈2019”Java原子操作第八章:AtomicReference介绍与使用》一章中介绍了什么是原子操作类AtomicReference<V>。
在《“全栈2019”Java原子操作第九章:atomic包下原子数组介绍与使用》一章中介绍了什么是原子数组AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray<E>。
在《“全栈2019”Java原子操作第十章:atomic包下字段原子更新器介绍》一章中介绍了什么是字段原子更新器AtomicIntegerFieldUpdater<T>、AtomicLongFieldUpdater<T>和AtomicReferenceFieldUpdater<T,V>。
在《“全栈2019”Java原子操作第十一章:CAS与ABA问题介绍与探讨》一章中介绍了CAS算法中存在的ABA问题。
现在介绍什么是带版本号的原子类AtomicStampedReference<V>。
1.为了解决CAS算法中的ABA问题
为了解决CAS算法中的ABA问题,Java提供了原子类AtomicStampedReference<V>和AtomicMarkableReference<V>来解决此问题。
在上一章《“全栈2019”Java原子操作第十一章:CAS与ABA问题介绍与探讨》中介绍了CAS算法中存在的ABA问题及解决方案。
本章主要说下AtomicStampedReference<V>原子类主要用法与实现。
如果想了解AtomicStampedReference<V>是如何解决ABA问题的小伙伴请查阅上述文章,谢谢。
2.带版本号的原子类AtomicStampedReference<V>
AtomicStampedReference<V>是一个带版本号的原子类,能以原子的方式更新对象的值。
其中,版本号的作用是记录对象被修改的次数。
3.AtomicStampedReference<V>对象创建方式
AtomicStampedReference<V>类通过构造方法AtomicStampedReference(V initialRef, int initialStamp)来创建对象:
参数
V initialRef:初始值。
int initialStamp:初始版本号。
示例
上面是创建了一个AtomicStampedReference<V>对象,初始值为0,初始版本号也为0。
源码分析
从创建AtomicStampedReference<V>对象开始,看看AtomicStampedReference<V>类是如何实现的。
首先,我们指定AtomicStampedReference<V>泛型为Integer类型:
然后,调用其构造方法AtomicStampedReference(V initialRef, int initialStamp),传递参数为(0, 0):
也就是initialRef=0,initialStamp=0。
其中,initialRef为初始值,initialStamp为初始版本号。通过“int initialStamp”参数我们知道了版本号只能是int类型:
接着,在AtomicStampedReference<V>构造方法内部执行“pair = Pair.of(initialRef, initialStamp);”语句:
其中,Pair是一对的意思。
一对是什么意思?
简单来说就是将两个数据合成一个数据对象。
什么意思,还是不明白。
你比如说,如果你要定义的类里面只定义一个属性,可以把类名起作One;
如果你要定义的类里面只定义两个属性,可以把类名起作Two;
如果你要定义的类里面只定义三个属性,可以把类名起作Three;
这样起名不规范也不学术,但是可以通过One、Two、Three明确知道里面有几个属性。
Pair只不过是比较学术一点的叫法,它实际上属于Two那种情况。里面有两个属性:reference(内存值)和stamp(版本号)。
接着“pair = Pair.of(initialRef, initialStamp);”语句往下看。
根据“Pair.of(initialRef, initialStamp);”语句来看,Pair类方法of(T reference, int stamp)可以用来创建Pair对象。
Pair类方法of(T reference, int stamp):
由于我们传递的参数(0, 0),所以这个泛型T变为Integer类型。
我们将泛型T换成Integer来看看:
接下来在of(T reference, int stamp)方法里面创建Pair对象并返回:
在Pair构造方法里面开始初始化reference属性和stamp属性:
其构造方法是private(私有)的,也就是我们无法通过new创建Pair对象,只能通过of(T reference, int stamp)方法来创建Pair对象。
于是,“Pair.of(initialRef, initialStamp);”语句执行完成,创建的Pair对象用于初始化AtomicStampedReference<V>对象中的pair变量:
至此,整个AtomicStampedReference<V>对象创建过程完毕。
下面,我们可以来看一下AtomicStampedReference<V>类与内部类Pair的UML类图:
通过类图可以看出AtomicStampedReference<V>对象里面维护了一个Pair对象,Pair对象里面维护了内存值和版本号。
原来,我们的内存值和版本号实际上存储在Pair对象中。
4.如何获取内存值?
调用AtomicStampedReference<V>对象的getReference()方法即可:
通过上面的分析得知,pair对象专门用来维护内存值,这里直接返回了pair对象里面的reference属性(即值)。
5.如何获取版本号?
调用AtomicStampedReference<V>对象的getStamp()方法即可:
同理,pair对象维护了版本号,直接返回pair对象里面的stamp属性即可。
6.如何设置新值和新版本号?
设置新值和新版本号可通过两种方式进行:
- 调用set(V newReference, int newStamp)方法
- 调用compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法
set(V newReference, int newStamp)与compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)区别在于:
- compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)返回boolean类型的值,可得知赋值是否成功;set(V newReference, int newStamp)无返回值。
- compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法体现了CAS算法,而set(V newReference, int newStamp)方法则没有。
实际应用中推荐使用compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法。
7.set(V newReference, int newStamp)方法
set(V newReference, int newStamp)方法源码:
set(V newReference, int newStamp)方法的作用是设置新值和新版本号。
参数
V newReference:新值。
int newStamp:新版本号。
示例
首先,创建AtomicStampedReference对象并指定初始值为0,初始版本号为0:
然后,调用set(V newReference, int newStamp)方法设置新值和新版本号:
最后,获取值和版本号:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。初始值和初始版本号通过set(1, 1)方法从0变为1。
源码分析
查阅set(V newReference, int newStamp)方法。
内部首先取得pair对象(pair对象里面维护了内存值和版本号),这是为了下面即将要进行的比较作准备:
然后判断我们传入的新值和新版本号和pair对象里面记录的值和版本号是否相同,若不相同则赋新值,否则什么也不做:
因为我们传入的值和版本号要是和之前的一样的话,再赋值就没有任何意义,所以这里进行了判断。避免了值相同的情况下还去操作内存。
8.compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法
compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法源码:
compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法的作用是利用CAS算法来设置新值和新版本号。
参数
V expectedReference:预期值。
V newReference:新值。
int expectedStamp:预期版本号。
int newStamp:新版本号。
示例
首先,创建AtomicStampedReference对象并指定初始值为0,初始版本号为0:
然后,调用其compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法并输出方法执行结果:
最后,获取并输出值和版本号:
例子书写完毕。
运行程序,执行结果:
从运行结果来看,符合预期。
源码分析
结合例子来分析compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法源码。
因为AtomicStampedReference泛型为Integer类型,所以将compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法里面的泛型改为Integer类型来帮助我们分析:
例子中有一个“value.compareAndSet(0, 1, 0, 1)”语句。
即expectedReference=0,newReference=1,expectedStamp=0,newStamp=1:
方法内部首先获取维护值和版本号的pair对象,此时pair对象里面的reference=0,stamp=0(这里写出来的原因是待会会代入下面的判断表达式中以便观察):
然后判断预期值与内存值是否相同:
如果预期值与内存值不相同,那么方法直接返回false;否则继续往下判断预期版本号与内存值的版本号是否相同。
将例子中的expectedReference=0和pair对象中的reference=0代入判断表达式中:
“0 == 0”表达式结果为true,继续往下判断预期版本号与内存值的版本号是否相同:
如果预期版本号与内存值的版本号不相同,那么方法直接返回false;否则继续往下判断新值与内存值是否相同。
将例子中的expectedStamp=0和pair对象中的stamp=0代入判断表达式中:
“0 == 0”表达式结果为true,继续往下判断新值与内存值是否相同和新版本号与内存值的版本号是否相同:
如果新值与内存值相同且新版本号与内存值的版本号也相同,那么方法直接返回true。
这样做的目的是可以少操作一次内存。因为原值与新值相同的情况下,没必要再去内存赋一遍一样的值。
如果新值与内存值不相同或新版本号与内存值的版本号不相同,那么方法继续往下执行“casPair(current, Pair.of(newReference, newStamp))”语句,该语句返回boolean类型结果。
将例子中的newReference=1和pair对象中的reference=0,newStamp=1和pair对象中的stamp=0代入判断表达式中:
“1 == 0 && 1 == 0”表达式结果为false,继续往下执行“casPair(current, Pair.of(newReference, newStamp))”语句:
其中,casPair(Pair<V> cmp, Pair<V> val)方法为实现了CAS算法的方法。
方法接收两个参数,第一个参数cmp为预期值,第二个参数val为新值。
将例子中的current变量以及由新值和新版本号构建的Pair对象代入“casPair(current, Pair.of(newReference, newStamp))”语句:
this中的Pair对象里的reference=0,stamp=0,即内存值;
cmp里的reference=0,stamp=0,即预期值;
val里的reference=1,stamp=1,即新值;
根据CAS算法“当且仅当内存值 == 预期值时,内存值 = 新值”原则,this中的Pair对象里的reference和stamp属性变为reference=1,stamp=1。
赋值成功,“value.compareAndSet(0, 1, 0, 1);”执行结果为true。
9.应用场景
AtomicStampedReference<V>应用在一些关心值每一次变化的场景中。
例如,非阻塞栈和非阻塞链表等等。
非阻塞栈在后面章节也会和大家一起从零开始写一遍,敬请关注。
最后,希望大家可以把这个例子照着写一遍,然后再自己默写一遍,方便以后碰到类似的面试题可以轻松应对。
祝大家编码愉快!
GitHub
本章程序GitHub地址:https://github.com/gorhaf/Java2019/tree/master/Thread/atomic/AtomicStampedReference<V>
总结
- AtomicStampedReference<V>是一个带版本号的原子类,能以原子的方式更新对象的值。
- AtomicStampedReference<V>可以解决CAS算法中的ABA问题。
- AtomicStampedReference<V>对象里面维护了一个Pair对象,Pair对象里面维护了内存值和版本号。实际上,内存值和版本号实际上存储在Pair对象中。
- getReference()方法的作用是获取内存值。
- getStamp()方法的作用是获取版本号。
- set(V newReference, int newStamp)方法的作用是设置新值和新版本号。
- compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)方法的作用是利用CAS算法来设置新值和新版本号。
至此,Java中AtomicStampedReference<V>相关内容讲解先告一段落,更多内容请持续关注。
答疑
如果大家有问题或想了解更多前沿技术,请在下方留言或评论,我会为大家解答。
上一章
“全栈2019”Java原子操作第十一章:CAS与ABA问题介绍与探讨
下一章
“全栈2019”Java原子操作第十三章:AtomicMarkableReference类
学习小组
加入同步学习小组,共同交流与进步。
- 方式一:关注头条号Gorhaf,私信“Java学习小组”。
- 方式二:关注公众号Gorhaf,回复“Java学习小组”。
全栈工程师学习计划
关注我们,加入“全栈工程师学习计划”。
版权声明
原创不易,未经允许不得转载!
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/22693.html