并发编程学习笔记 之 原子操作类AtomicReference、AtomicStampedReference详解

导读:本篇文章讲解 并发编程学习笔记 之 原子操作类AtomicReference、AtomicStampedReference详解,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、前言

  在《并发编程学习笔记 之 原子操作类AtomicInteger详解》中,我们学习了原子操作类AtomicInteger的用法,类似的还有AtomicLong、AtomicBoolean等类型,这些都是针对基本类型定义的原子性操作,那么针对对象是否有原子性操作呢?这一节,我们就开始学习原子操作类AtomicReference,看看如何实现针对对象的原子性操作。

二、AtomicReference的直观体验

  这里我们改写了《Java高并发编程详解:深入理解并发核心库》一书中的例子,设计一个个人银行账号资金变化的场景:

  • 个人账号被设计为不可变对象,一旦创建就无法进行修改。
  • 个人账号类只包含两个字段:账号名、现金数字。
  • 为了便于验证,我们约定个人账号的现金只能增多而不能减少。

  首先,我们设计一个个人银行账号类,包括账号名、现金数字两个字段,如下所示:

@Data
@AllArgsConstructor
public class DebitCard {

    private final String account; //账号名
    private final int amount;   //现金数字

}

   第一种场景: 使用volatile 关键字修饰DebitCard 对象,然后模拟多用户(多线程)同时转钱到账户,实现如下:

public class AtomicReferenceTest {

    private static final int THREADS_CONUT = 20;
    static volatile DebitCard debitCard = new DebitCard("Alex",0);

    public static void increase() {
        final DebitCard dc = debitCard;
        DebitCard newDc = new DebitCard(dc.getAccount(),dc.getAmount()+10);
        debitCard = newDc;
    }
    public static void main(String[] args) {
        for(int i = 0; i < THREADS_CONUT; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //为了避免线程快速执行完成,变成了类似串行执行的效果
                        TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    increase();
                }
            });
            t.start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
       System.out.println("debitCard:" + debitCard.getAmount());
    }
}

  多次运行上述代码,会发现,无法保证每次结果都是预期的结果,原因在上一节讲解原子操作类AtomicInteger时,已经分析了。如果采用想保证操作的原子性,我们需要借助synchronized关键字实现,修改increase方法即可:

public synchronized static void increase() {
   final DebitCard dc = debitCard;
    DebitCard newDc = new DebitCard(dc.getAccount(),dc.getAmount()+10);
    debitCard = newDc;
}

   第二种场景: 借助我们这节的主角原子操作类AtomicReference实现非阻塞的原子操作,本质还是基于CAS算法实现,具体实现如下:

public class AtomicReferenceTest {

    private static final int THREADS_CONUT = 20;
    private static AtomicReference<DebitCard> ref = new AtomicReference<>(new DebitCard("Alex",0));

    public static void increase2() {
        //每一次对AtomicReference的更新操作,我们都采用CAS这一乐观非阻塞的方式进行,
        // 因此也会存在对DebitCard对象引用更改失败的问题
        DebitCard dc = null;
        DebitCard newDC = null;
        do{
            // 获取AtomicReference的当前值
            dc = ref.get();
            // 基于AtomicReference的当前值创建一个新的DebitCard
            newDC = new DebitCard(dc.getAccount(), dc.getAmount() + 10);
            // 基于CAS算法更新AtomicReference的当前值
        }while (!ref.compareAndSet(dc, newDC));
    }

    public static void main(String[] args) {
        for(int i = 0; i < THREADS_CONUT; i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //为了避免线程快速执行完成,变成了类似串行执行的效果
                        TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    increase2();
                }
            });
            t.start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("debitCard:" + ref.get().getAmount());
    }
}

  通过CAS算法实现了原子性操作。

三、AtomicReference的常见用法

1、构造函数

和AtomicInteger构造参数类似,包括无参和有参构造函数如下:

  • AtomicReference():当使用无参构造函数创建AtomicReference对象的时候,需要再次调用set()方法为AtomicReference内部的value指定初始值。
  • AtomicReference(V initialValue):创建AtomicReference对象时顺便指定初始值。
2、compareAndSet()方法
  • compareAndSet(V expect, V update):原子性地更新AtomicReference内部的value值,其中expect代表当前AtomicReference的value值,update则是需要设置的新引用值。该方法会返回一个boolean的结果,当expect和AtomicReference的当前值不相等时,修改会失败,返回值为false,若修改成功则会返回true。

  其他方法,和AtomicInteger类中的类似,这里不再一一列举,详细的可以参看源码。

  关于compareAndSet()方法的实现,和AtomicInteger中的方法类似,也是借助unsafe对象的compareAndSwapObject()方法实现原子性操作。在上述的示例中的第二种场景中,我们利用自旋+CAS算法,实现了increase2()方法的原子性操作。

四、CAS算法ABA问题

  在上述场景中,我们假设了个人账号的现金只能增多而不能减少,在现实生活中,个人账户的现金肯定是有增有减的,那么如果是这种情况下,上述实现会有什么问题呢?这就是CAS算法的ABA问题。

  CAS算法ABA问题描述如下:如果有T1、T2两个线程,当T1读取内存变量为A,T2修改内存变量为B,T2修改内存变量为A,这时T1再CAS操作A时是可行的。但实际上在T1第二次操作A时,已经被其他线程修改过了。

  对于ABA问题,比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;在进行CAS操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS才能执行成功。Java语言中的AtomicStampedReference类便是使用版本号来解决ABA问题的。

五、AtomicStampedReference

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

  • AtomicStampedReference构造函数,在创建AtomicStampedReference时除了指定引用值的初始值之外还要给定初始的stamp。
public AtomicStampedReference(V initialRef, int initialStamp) {
   pair = Pair.of(initialRef, initialStamp);
}

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);
   }
}
  • getReference():获取当前引用值,等同于其他原子类型的get方法。
public V getReference() {
    return pair.reference;
}
  • getStamp():获取当前引用值的stamp数值。
 public int getStamp() {
    return pair.stamp;
}
  • V get(int[] stampHolder):这个方法的意图是获取当前值以及stamp值,但是Java不支持多值的返回,并且在AtomicStampedReference内部Pair被定义为私有的,因此这里就采用了传参的方式来解决,其中stampHolder参数用于获取stamp,返回值为reference。这个设计有点儿奇怪。
public V get(int[] stampHolder) {
   Pair<V> pair = this.pair;
   stampHolder[0] = pair.stamp;
   return pair.reference;
}
  • compareAndSet(V expectedReference, V newReference,int expectedStamp, int newStamp):对比并且设置当前的引用值,这与其他的原子类型CAS算法类似,只不过多了expectedStamp和newStamp,只有当expectedReference与当前的Reference相等,且expectedStamp与当前引用值的stamp相等时才会发生设置,否则set动作将会直接失败。weakCompareAndSet()方法,和该方法用法完全一样。
  • set(V newReference, int newStamp):设置新的引用值以及stamp。
  • attemptStamp(V expectedReference, int newStamp):该方法的主要作用是为当前的引用值设置一个新的stamp,该方法为原子性方法。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68709.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!