侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

Java并发编程之原子操作类AtomicStampedReference实战详解

孔子说JAVA
2022-06-11 / 0 评论 / 0 点赞 / 63 阅读 / 10,842 字 / 正在检测是否收录...

并发编程原子类完整教程:Java并发编程之原子操作类实战教程

AtomicStampedReference 是java.util.concurrent(简写为JUC)包下的类,和 AtomicReference 等类是一样(这些类也是JUC下的),也是基于CAS无锁理论实现的,AtomicStampedReference 与 AtomicReference 不同之处在于 AtomicStampedReference 在内部同时保存一个对象引用和一个时间戳。引用和时间戳可以通过 compareAndSet() 方法使用原子性的比较和交换操作进行交换。AtomicStampedReference 是为解决A-B-A问题而设计的,该类提供了对象引用的非阻塞原子性读写操作,并且提供了其他一些高级的用法。

  • 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是A-B-A问题。
  • AtomicStampedReference 作⽤是对”对象”进⾏原⼦操作,提供了⼀种读和写都是原⼦性的对象引⽤变量。原⼦意味着多个线程试图改变同
    ⼀个 AtomicStampedReference (例如⽐较和交换操作) 不会使 AtomicStampedReference 处于不⼀致的状态。
  • 除了 AtomicStampedReference 类,AtomicMarkableReference 原子类也可以解决A-B-A问题,它不是维护一个版本号,而是维护一个boolean类型的标记,用法没有AtomicStampedReference灵活。因此也只是在特定的场景下使用。

1、A-B-A问题

ABA问题描述

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,就是说一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,使用CAS进行检查时会发现它的值没有发生变化,就误以为是原来的那个A,但是实际上却变化了。这就是有名的A-B-A问题。

ABA问题的后果

ABA问题会带来什么后果呢?我们来举个例子。

一个小偷,把别人家的钱偷了之后又还回去了,钱还是原来的钱吗?且小偷也做了违法的事情。ABA问题也一样,如果不好好解决就会带来大量的问题。最常见的就是资金问题,也就是别人如果挪用了你的钱,在你发现之前又还了回来。但是别人却已经触犯了法律。

再举个例子:

你倒了一杯水放桌子上,干了点别的事,然后同事把你水喝了又给你重新倒了一杯水,你回来看水还在,拿起来就喝,如果你不管水中间是否被人喝过,只关心水还在,这就是ABA问题。如果你是一个讲卫生讲文明的人,不但关心水在不在,还关心在你离开的时候水有没有被人动过,因为你是程序员,所以就想起了放了张纸在旁边,写上初始值0,别人喝水前麻烦先做个累加才能喝水。

ABA问题的解决思路

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。AtomicStampedReference 就是类似的原理。

2、AtomicStampedReference 主要方法

2.1 构造方法

AtomicStampedReference在构建的时候需要一个类似于版本号的int类型变量stamped,每一次针对共享数据的变化都会导致该 stamped 的变化(stamped 需要应用程序自身去负责,AtomicStampedReference并不提供,一般使用时间戳作为版本号),因此就可以避免ABA问题的出现,AtomicStampedReference的使用也是极其简单的,创建时我们不仅需要指定初始值,还需要设定stamped的初始值,在AtomicStampedReference的内部会将这两个变量封装成Pair对象。

// 构造函数,初始化引用和版本号
public AtomicStampedReference(V initialRef, int initialStamp)

2.2 主要方法

// 以原子方式获取当前引用值
public V getReference()
 
// 以原子方式获取当前版本号
public int getStamp()
 
// 以原子方式获取当前引用值和版本号
public V get(int[] stampHolder)
 
// 以原子的方式同时更新引用值和版本号
// 当期望引用值不等于当前引用值时,操作失败,返回false
// 当期望版本号不等于当前版本号时,操作失败,返回false
// 在期望引用值和期望版本号同时等于当前值的前提下
// 当新的引用值和新的版本号同时等于当前值时,不更新,直接返回true
// 当新的引用值和新的版本号不同时等于当前值时,同时设置新的引用值和新的版本号,返回true
public boolean weakCompareAndSet(V  expectedReference,
                                 V  newReference,
                                 int expectedStamp,
                                 int newStamp)
 
// 以原子的方式同时更新引用值和版本号
// 当期望引用值不等于当前引用值时,操作失败,返回false
// 当期望版本号不等于当前版本号时,操作失败,返回false
// 在期望引用值和期望版本号同时等于当前值的前提下
// 当新的引用值和新的版本号同时等于当前值时,不更新,直接返回true
// 当新的引用值和新的版本号不同时等于当前值时,同时设置新的引用值和新的版本号,返回true
public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp)
 
// 以原子方式设置引用的当前值为新值newReference
// 同时,以原子方式设置版本号的当前值为新值newStamp
// 新引用值和新版本号只要有一个跟当前值不一样,就进行更新
public void set(V newReference, int newStamp)
 
// 以原子方式设置版本号为新的值
// 前提:引用值保持不变
// 当期望的引用值与当前引用值不相同时,操作失败,返回fasle
// 当期望的引用值与当前引用值相同时,操作成功,返回true
public boolean attemptStamp(V expectedReference, int newStamp)
 
// 使用`sun.misc.Unsafe`类原子地交换两个对象
private boolean casPair(Pair<V> cmp, Pair<V> val)

3、AtomicStampedReference 演示示例

3.1 演示示例1

本示例演示 AtomicStampedReference 的基础用法。

import java.util.concurrent.atomic.AtomicStampedReference;


public class AtomicStampedReferenceTest {
    public static void main(String[] args) {
        //1、 attemptStamp()方法:如果当前引用 == 预期引用,则以原子方式将该标志的值设置为给定的更新值。
        String expectedReference = "abc";
        String newReference = "def";
        AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference<>(expectedReference,1);
        Boolean bool = atomicStampedReference.attemptStamp("abc" , 5);
        System.out.println("Boolean: " + bool);
        System.out.println("atomicStampedReference : reference  " + atomicStampedReference.getReference());
        System.out.println("atomicStampedReference : stampe  " + atomicStampedReference.getStamp());

        //2、compareAndSet()方法:如果当前引用 == 预期引用,并且当前标志等于预期标志,则以原子方式将该引用和该标志的值设置为给定的更新值。
        AtomicStampedReference<String> atomicStampedReference1 = new AtomicStampedReference<>(expectedReference,1);
        Boolean bool1 = atomicStampedReference1.compareAndSet("abc" , "edf",1,10);
        System.out.println("Boolean: " + bool1);
        System.out.println("atomicStampedReference : reference  " + atomicStampedReference1.getReference());
        System.out.println("atomicStampedReference : stampe  " + atomicStampedReference1.getStamp());
        
        //3、set()方法:无条件地同时设置该引用和标志的值。
        AtomicStampedReference<String> atomicStampedReference2 = new AtomicStampedReference<>(expectedReference , 10);
        System.out.println("atomicStampedReference : reference  " + atomicStampedReference2.getReference());
        System.out.println("atomicStampedReference : stampe  " + atomicStampedReference2.getStamp());
        atomicStampedReference2.set(newReference,100);
        System.out.println("atomicStampedReference : reference  " + atomicStampedReference2.getReference());
        System.out.println("atomicStampedReference : stampe  " + atomicStampedReference2.getStamp());
    }
}

3.2 演示示例2

本示例演示 AtomicStampedReference 的基础用法。

public class MyAtomicStampedReference {
    //版本是,以后每修改一次都会增加1,如果修改时和自己预料的版本不一致就会修改失败
    static AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference("tom",10);

    public static void main(String[] args) {
        int oldStamp = atomicStampedReference.getStamp();
        String oldReference = atomicStampedReference.getReference();

        System.out.println("初始化之后的版本:"+oldStamp);
        System.out.println("初始化之后的值:"+oldReference);

        String newReference = "jerry";

        boolean b =atomicStampedReference.compareAndSet(oldReference,newReference,1,oldStamp+1);
        if(!b){
            System.out.println("版本戳不一致,无法修改Reference的值");
        }
        b =atomicStampedReference.compareAndSet(oldReference,newReference,oldStamp,oldStamp+1);
        if(b){
            System.out.println("版本戳一致,修改reference的值为jerry");
        }
        System.out.println("修改成功之后的版本:"+atomicStampedReference.getStamp());
        System.out.println("修改成功之后的值:"+atomicStampedReference.getReference());
    }
}

测试结果:

初始化之后的版本:10
初始化之后的值:tom
版本戳不一致,无法修改Reference的值
版本戳一致,修改reference的值为jerry
修改成功之后的版本:11
修改成功之后的值:jerry

3.3 演示示例3

AtomicStampedReference 的引入是为了解决 AtomicReference 的ABA问题。下面是使用 AtomicStampedReference 解决CAS机制中ABA问题的例子:

  • synchronized是一种阻塞式的解决方案,同一时刻只能有一个线程真正在工作,其他线程都将陷入阻塞,因此这并不是一种高效的解决方案,这个时候就可以利用 AtomicReference 的非阻塞原子性解决方案提供更加高效的方式了。
  • AtomicStampedReference 同 AtomicReference 类似,同时还解决了CAS机制中的ABA问题。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceTest {
    private static AtomicStampedReference<Integer> atomicref = new AtomicStampedReference<Integer>(100, 0);
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                    boolean success = atomicref.compareAndSet(100, 101, atomicref.getStamp(), atomicref.getStamp() + 1);
                    System.out.println("t1 = " + success);
                    success = atomicref.compareAndSet(101, 100, atomicref.getStamp(), atomicref.getStamp() + 1);
                    System.out.println("t1 = " + success);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    int stamp = atomicref.getStamp();
                    System.out.println("Before sleep:stamp = " + stamp);
                    TimeUnit.SECONDS.sleep(2);
                    boolean success = atomicref.compareAndSet(100, 101, stamp, stamp + 1);
                    System.out.println("t2 = " + success);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

测试结果:

Before sleep:stamp = 0
t1 = true
t1 = true
t2 = false

4、源码分析

AtomicStampedReference 的源码比较简单,定义了一个内部类Pair,主要用来存放引用值和版本号stamp。下面对 AtomicStampedReference 部分源码进行解析。

4.1 实现基础 - 内部类的定义

AtomicStampedReference是一个泛型类,并且没有继承任何方法和实现任何接口,通过一个内部类Pair对象来存放引用值和版本号stamp。

public class AtomicStampedReference<V>{
    // 定义了一个内部类Pair,主要用来存放引用值和版本号stamp
    private static class Pair<T> {
        final T reference;  // 变量引用
        final int stamp;   // 版本号
        private Pair(T reference, int stamp){
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp){
            return new Pair<T>(reference, stamp);
        }
    }
}

4.2 实现基础 - 主要字段

// 当前对象,包括对象引用和版本号
private volatile Pair<T> pair;

// Unsafe是所有并发工具的基础工具类,设置为使用Unsafe.compareAndSwapInt进行更新,方法中多处需要使用unsafe变量
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();

// 利用unsafe的本地方法来获取对象在内存的偏移地址
private static final long pairOffset = objectField(UNSAFE, "pair", AtomicStampedReference.class);

4.3 实现基础 - 构造方法

构造方法利用 initialRef 和 initialStamp 生成当前对象,包括对象引用和版本号。

/**
 * 构造方法
 * @param initialRef  初始变量引用
 * @param initialStamp  版本号
 */
public AtomicStampedReference(V initialRef, int initialStamp) {
   pair = Pair.of(initialRef, initialStamp);
}

4.4 实现基础 - 主要方法

4.4.1 工具方法

//利用unsafe的CAS方法来对对象进行交换和设置,AtomicStampedReference内部很多方法基于此方法实现的
private boolean casPair(Pair<V> cmp, Pair<V> val){
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

static long objectField(sun.misc.Unsafe UNSAFE, String field, Class<?> klazz){
    try{
        return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
    }catch(NoSuchFieldException e){
        NoSuchFieldError error = new NoSuchFieldError(field);
        error.initCause(e);
        throw error;
    }
}

4.4.2 get和set方法

  • getReference()方法, 以原子方式获取当前引用值。
  • getStamp()方法, 以原子方式获取当前版本号。
  • get(int[] stampHolder)方法,这个方法的意图是获取当前值以及stamp值,但是Java不支持多值的返回,并且在AtomicStampedReference内Pair被定义为私有的,因此在此处就采用了传参的方式来解决这个问题。
  • set(V newReference, int newStamp)方法,以原子方式设置引用的当前值为新值newReference,同时,以原子方式设置版本号的当前值为新值newStamp,新引用值和新版本号只要有一个跟当前值不一样,就进行更新。
// 以原子方式获取当前引用值
public V getReference(){
    return pair.reference;
}

// 以原子方式获取当前版本号
public int getStamp(){
    return pair.stamp;
}

// 以原子方式获取当前引用值和版本号
public V get(int[] stampHolder){
    Pair<V> pair = this.pair;
    stampHolder[0] = pair.stamp;
    return pair.reference;
}

// 以原子方式设置引用的当前值为新值newReference
// 同时,以原子方式设置版本号的当前值为新值newStamp
// 新引用值和新版本号只要有一个跟当前值不一样,就进行更新
public void set(V newReference, int newStamp){
    Pair<V> current = pair;
    if(newReference != current.reference || newStamp != current.stamp){
        this.pair = Pair.of(newReference, newStamp);
    }
}

4.4.3 比较设置方法

比较设置方法有 compareAndSet 和 weakCompareAndSet。

  • 原子的将引用值设置为 newValue,如果旧值 == expectedValue,设置成功返回 true,否则返回 false。这个方法是支持CAS操作的,同一个时间只有一个线程调用 compareAndSet()方法。
  • compareAndSet方法,主要是比较并且设置当前的引用值,这和其他原子类的CAS算法相同。这里,唯一区别的地方,是需要去比较版本号,即stamp的值。当expectedReference与当前的引用值相等,并且,expectedStamp与当前的stamp相等时,执行更新操作,把当前的引用值设置为newReference,当前的版本号设置为newStamp,然后返回true,否则,返回false.
  • weakCompareAndSet() 方法与 compareAndSet() 类似,但 weakCompareAndSet() 不会插入内存屏障,不能保障volatile的原子性
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp){
    Pair<V> current = pair;
    return expectedReference == current.reference &&
        expectStamp == current.stamp &&
        ((newReference == current.reference &&
         newStamp == current.stamp) || 
         casPair(current, Pair.of(newReference, newStamp)));
}

public boolean weakCompareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp){
    return compareAndSet(expectedReference, newReference, expectedStamp, newStamp);
}

unsafe是通过Unsafe.getUnsafe()返回的一个Unsafe对象。通过Unsafe的CAS函数对Object引用类型数组的元素进行原子操作。如compareAndSetRaw()就是调用Unsafe的CAS函数。

4.4.4 更新版本号方法

该方法,主要是原子性地更新版本号的值,首先会比较当前引用的对象是不是expectedReference,如果是则更新版本号的值,并且返回true,否则返回false。

  • 以原子方式设置版本号为新的值
  • 前提:引用值保持不变
  • 当期望的引用值与当前引用值不相同时,操作失败,返回fasle
  • 当期望的引用值与当前引用值相同时,操作成功,返回true
public boolean attempStamp(V expectedReference, int newStamp){
    Pair<V> current = pair;
    return expectedReference == current.reference &&
        (newStamp == current.stamp || casPair(current, Pair.of(expectedReference, newStamp)));
}
0

评论区