Java并发26:Atomic系列-ABA问题-带版本戳的原子引用类型AtomicStampedReference与AtomicMarkableReference

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值