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的具体用法就不再赘述了,有兴趣的博友可以自行查看源代码进行学习。

 

posted @ 2021-08-26 11:29  姚春辉  阅读(315)  评论(0编辑  收藏  举报