[超级链接:Java并发学习系列-绪论]
[系列概述: Java并发22:Atomic系列-原子类型整体概述与类别划分]
本章主要对带版本戳的原子引用类型进行学习。
1.ABA问题
带版本戳的原子引用类型主要是为了解决ABA问题而设计的,下面对ABA问题进行简单描述和示例。
ABA问题概述:
- 变量X的值为A.
- [Thread-1]准备更新变量X的值,预期值为A,准备更新为A,即A ==> B.
- [Thread-2]对变量X进行了两次更新操作:A ==> B B ==> A
- [Thread-1]判断此时变量X仍然为预期A,可以更新,于是进行更新操作:A ==> B
上述过程中,虽然看起来变量X仍然为预期A,其实此时的A并不是之前的那个预期A,它是经过A ==> B B ==> A过程之后的新的A。
下面通过一段简短的代码模拟这种ABA过程:
//ABA问题
System.out.println("==========ABA问题:");
AtomicReference<String> reference = new AtomicReference<>("A");
new Thread(() -> {
//获取期望值
String expect = reference.get();
//打印期望值
System.out.println(Thread.currentThread().getName() + "---- expect: " + expect);
try {
//干点别的事情
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印实际值
System.out.println(Thread.currentThread().getName() + "---- actual: " + reference.get());
//进行CAS操作
boolean result = reference.compareAndSet("A", "X");
//打印操作结果
System.out.println(Thread.currentThread().getName() + "---- result: " + result + " ==》 final reference = " + reference.get());
}).start();
new Thread(() -> {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//进行ABA操作
System.out.print(Thread.currentThread().getName() + "---- change: " + reference.get());
reference.compareAndSet("A", "B");
System.out.print(" -- > B");
reference.compareAndSet("B", "A");
System.out.println(" -- > A");
}).start();
运行结果:
==========ABA问题:
Thread-0---- expect: A
Thread-1---- change: A -- > B -- > A
Thread-0---- actual: A
Thread-0---- result: true ==》 final reference = X
2.带版本戳的原子引用类型
为了解决上述的ABA问题,Java提供了两种带版本戳的原子引用类型:
- AtomicStampedReference:带版本戳的原子引用类型,版本戳为int类型。
- AtomicMarkableReference:带版本戳的原子引用类型,版本戳为boolean类型。
本章主要以AtomicStampedReference作为学习对象。
3.方法学习
AtomicStampedReference提供的方法如下:
1.AtomicStampedReference<>(V initialRef, int initialStamp)
- 带版本戳的原子引用类型没有无参的构造函数。
- 带版本戳的原子引用类型只有这个构造函数,要求必须设置初始的引用对象以及版本戳。
2.getReference()与getStamp()
- getReference():获取引用对象。
- getStamp():获取版本戳。
3.set(V newReference, int newStamp)
重新设置引用对象以及版本戳。
4.attemptStamp(V expectedReference, int newStamp)
如果引用对象为期望值,则重新设置新的版本戳。
5.compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
如果引用对象为期望值,并且版本戳正确,则赋新值并修改版本戳。
6.get(int[] stampHolder)
- 获取引用当前值以及版本戳
- 注意参数为长度至少为1的数组类型
- 其中:引用值为通过return返回,版本戳存放在stampHolder[0]中
- 参数使用数组类型的原因:需要将版本戳存放在参数中,而基本数据类型无法进行引用传递,但是数组可以。
实例代码:
//AtomicStampedReference的方法汇总:
System.out.println("\n=========AtomicStampedReference的方法汇总:");
//构造方法:AtomicStampedReference<>(V initialRef, int initialStamp)
System.out.println("构造方法:AtomicStampedReference<>(V initialRef, int initialStamp)");
AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>("David", 1);
//getStamp和getReference:获取版本戳和引用对象
System.out.println("\ngetReference():获取引用对象的值----" + stampedReference.getReference());
System.out.println("getStamp():获取引用对象的值的版本戳----" + stampedReference.getStamp());
//set(V newReference, int newStamp):无条件的重设引用和版本戳的值
stampedReference.set("Joke", 0);
System.out.println("\nset(V newReference, int newStamp):无条件的重设引用和版本戳的值---[reference:"
+ stampedReference.getReference() + ",stamp:" + stampedReference.getStamp() + "]");
//attemptStamp(V expectedReference, int newStamp)
stampedReference.attemptStamp("Joke", 11);
System.out.println("\nattemptStamp(V expectedReference, int newStamp):如果引用为期望值,则重设版本戳---[reference:"
+ stampedReference.getReference() + ",stamp:" + stampedReference.getStamp() + "]");
//compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
System.out.println("\ncompareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp):" +
"\n如果引用为期望值且版本戳正确,则赋新值并修改版本戳:");
System.out.println("第一次:" + stampedReference.compareAndSet("Joke", "Tom", 11, 12));
System.out.println("第二次:" + stampedReference.compareAndSet("Tom", "Grey", 11, 12));
System.out.println("weakCompareAndSet不再赘述");
//get(int[] stampHolder):通过版本戳获取引用当前值
//参数为数组类型是因为基本类型无法传递引用,需要使用数组类型
int[] stampHolder = new int[10];
String aRef = stampedReference.get(stampHolder);
System.out.println("\nget(int[] stampHolder):获取引用和版本戳,stampHolder[0]持有版本戳---[reference=" + aRef + ",stamp=" + stampHolder[0] + "].");
运行结果:
=========AtomicStampedReference的方法汇总:
构造方法:AtomicStampedReference<>(V initialRef, int initialStamp)
getReference():获取引用对象的值----David
getStamp():获取引用对象的值的版本戳----1
set(V newReference, int newStamp):无条件的重设引用和版本戳的值---[reference:Joke,stamp:0]
attemptStamp(V expectedReference, int newStamp):如果引用为期望值,则重设版本戳---[reference:Joke,stamp:11]
compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp):
如果引用为期望值且版本戳正确,则赋新值并修改版本戳:
第一次:true
第二次:false
weakCompareAndSet不再赘述
get(int[] stampHolder):获取引用和版本戳,stampHolder[0]持有版本戳---[reference=Tom,stamp=12].
4.解决ABA问题
通过上面的学习,基本掌握了AtomicStampedReference提供的方法。
下面通过一个简单的实例模拟解决ABA问题:
//通过版本戳解决ABA问题
System.out.println("\n==========通过版本戳解决ABA问题:");
AtomicStampedReference<String> stampedRef = new AtomicStampedReference<>("A", 1);
new Thread(() -> {
//获取期望值
String expect = stampedRef.getReference();
//获取期望版本戳
Integer stamp = stampedRef.getStamp();
//打印期望值和期望版本戳
System.out.println(Thread.currentThread().getName() + "---- expect: " + expect + "-" + stamp);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印实际值和实际版本戳
System.out.println(Thread.currentThread().getName() + "---- actual: " + stampedRef.getReference() + "-" + stampedRef.getStamp());
//进行CAS操作(带版本戳)
boolean result = stampedRef.compareAndSet("A", "X", stamp, stamp + 1);
//打印操作结果
System.out.println(Thread.currentThread().getName() + "---- result: " + result + " ==》 final reference = " + stampedRef.getReference() + "-" + stampedRef.getStamp());
}).start();
new Thread(() -> {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
进行ABA操作(带版本戳)
System.out.print(Thread.currentThread().getName() + "---- change: " + stampedRef.getReference() + "-" + stampedRef.getStamp());
stampedRef.compareAndSet("A", "B", stampedRef.getStamp(), stampedRef.getStamp() + 1);
System.out.print(" -- > B" + "-" + stampedRef.getStamp());
stampedRef.compareAndSet("B", "A", stampedRef.getStamp(), stampedRef.getStamp() + 1);
System.out.println(" -- > A" + "-" + stampedRef.getStamp());
}).start();
运行结果:
==========通过版本戳解决ABA问题:
Thread-2---- expect: A-1
Thread-3---- change: A-1 -- > B-2 -- > A-3
Thread-2---- actual: A-3
Thread-2---- result: false ==》 final reference = A-3
通过分析运行结果,发现带版本戳的原子引用类型确实能够解决ABA问题。
5.关于AtomicMarkableReference
关于AtomicMarkableReference的原理其实是与AtomicStampedReference类似的。
因为其版本戳只是boolean类型,所以导致版本状态只有两个:true或者false。
所以,我更倾向于称呼AtomicMarkableReference为带标记的原子引用类型。
- 版本戳 = true,表示此引用被标记。
- 版本戳 = false,表示此引用未被标记。
关于AtomicStampedReference的具体用法就不再赘述了,有兴趣的博友可以自行查看源代码进行学习。