Java并发26:Atomic系列-ABA问题-带版本戳的原子引用类型AtomicStampedReference与AtomicMarkableReference
本章主要对带版本戳的原子引用类型进行学习。
1.ABA问题
带版本戳的原子引用类型主要是为了解决ABA问题而设计的,下面对ABA问题进行简单描述和示例。
ABA问题概述:
变量X的值为A.
- [Thread-1]准备更新变量reference的值,预期值为A,准备更新为X,即A ==> X.
- [Thread-2]对变量reference进行了两次更新操作:A ==> B B ==> A
- [Thread-1]判断此时变量reference仍然为预期A,可以更新,于是进行更新操作:A ==> X
- 上述过程中,虽然看起来变量reference仍然为预期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类型。Stamped adj. 铭刻的;盖上邮戳的;顿足的
- AtomicMarkableReference:带版本戳的原子引用类型,版本戳为boolean类型。 Markable adj.<罕>可被标记的
本章主要以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的具体用法就不再赘述了,有兴趣的博友可以自行查看源代码进行学习。