并发编程之AtomicReference

3,581 阅读7分钟

简介

今天我们来学习另外几个开发过程中可能会用到的原子类,或者在面试的时候会被问到的类。

分别是:

1、AtomicReference

2、AtomicStampedReference

3、AtomicArray

4、AtomicFieldUpdater

我们在前两节学习的类,都是针对基础类型地原子性读写而设计的,这以上几个都是为引用类型地原子性操作而设计的,那如何使用他们呢?使用场景又是如何呢?


AtomicReference

该类提供了对象引用的非阻塞原子性读写操作

那老规矩,我们还是来看看它的源码:

public class AtomicReference<V> implements java.io.Serializable {
    private static final long serialVersionUID = -1848883965231344442L;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
	//volatile修饰了一个泛型的value属性
    private volatile V value;
}

正如介绍所说:提供了对象引用的原子性操作;其API的话,和AtomicInteger基本一致,我这就不逐一列举了。

我们来简单模拟一个抽奖:

各位读者都开了一家很大的公司,每逢节假日就搞活动,抽奖送汽车(有钱就是这么任性);假设有100辆汽车,参与抽奖的人很多,虽然各位都很有钱,也很任性,但是本次采购的车车只有这么多,所以得精确控制中奖人数。

废话不多说,我们直接上代码(真实系统的抽奖肯定更为完善):

//定义我们的奖品类
@Data
public class Prize {

    /**
     * 一等奖:小米汽车
     */
    private String level;

    /**
     * 数量
     */
    private int count;

    public Prize(String level, int count) {
        this.level = level;
        this.count = count;
    }
}

我们先来看看非线程安全的代码测试:

public static void main(String[] args) {
        Prize prize = new Prize("小米汽车", 100);
        AtomicInteger atomicInteger = new AtomicInteger();
        IntStream.range(0, 300).forEach(
                value -> {
                    new Thread(
                            () -> {
                                //①获得当前还剩多少号
                                int count = prize.getCount();
                                if (count > 0) {
                                    //②对剩余号源减1,并更新回奖池
                                    prize.setCount(count - 1);
                                    atomicInteger.incrementAndGet();
                                    log.info("当前线程:{},抢到了 {} 号", Thread.currentThread().getName(), count);
                                }
                            }
                    ).start();
                }
        );
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("中奖人数:{}", atomicInteger.get());
    }

以上代码存在线程不安全。其不安全的本质是:当线程1、线程2同时执行到处,假如都获得了当前剩余号数10,继续往下执行到处,都对其进行了减1,最终两个线程更新回去却是9;针对这种情况我们有很多解决方案,这里我选择使用AtomicReference类进行测试:

public static void main(String[] args) {
  		//将我们的初始奖池封装到AtomicReference中
        AtomicReference<Prize> reference = new AtomicReference<>(new Prize("小米汽车", 100));
        AtomicInteger atomicInteger = new AtomicInteger(0);
        IntStream.range(0, 300).forEach(
                value -> {
                    new Thread(
                            () -> {
                                //①获得当前还剩多少号的对象
                                final Prize prize = reference.get();
                                if (prize.getCount() > 0) {
                                  	//②对剩余号源进行减1
                                    Prize prizeNew = new Prize(prize.getLevel(), reference.get().getCount() - 1);
                                  	//③将数据更新到奖池
                                    if (reference.compareAndSet(prize, prizeNew)) {
                                        log.info("当前线程:{},抢到了 {} 号", Thread.currentThread().getName(), prize.getCount());
                                        atomicInteger.incrementAndGet();
                                    }
                                }
                            }
                    ).start();
                }
        );
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("中奖人数:{}", atomicInteger.get());
    }

观察上面代码:虽然①②处也会出现之前基础版本的情况,但是最终将数据刷新回奖池的时候,如果prize对象的引用已经被其他线程修改,则当前线程执行**reference.compareAndSet(prize, prizeNew)**会更新失败。对于这个线程来说,好气呀,手都伸进抽奖箱了,还是没有抢到大奖;对于老板来说无伤大雅,只管送出指定数量即可;针对这种可以搞一个while循环让线程进行重试(摸一次就可以了嘛,还想摸多少次?)

是不是对AtomicReference的使用比较熟悉了呢?

接下来我们对AtomicReference、显示锁Lock、synchronized进行性能大PK:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
public class SynchronizedVsAtomicReference {

    @State(Scope.Group)
    public static class MonitorSync {
        private final Prize prize = new Prize("小米汽车", 100);

        public void syncDeduct() {
            synchronized (SynchronizedVsAtomicReference.class) {
                int count = prize.getCount();
                if (count > 0) {
                    prize.setCount(count - 1);
                }
            }
        }
    }

    @State(Scope.Group)
    public static class MonitorReference {

        private final AtomicReference<Prize> reference = new AtomicReference<>(new Prize("小米汽车", 100));

        public void referenceDeduct() {
            final Prize p = reference.get();
            final Prize newP = new Prize(p.getLevel(), p.getCount() - 1);
            reference.compareAndSet(p, newP);
        }
    }

    @State(Scope.Group)
    public static class MonitorLock {
        private final Prize prize = new Prize("小米汽车", 100);
        private final Lock lock = new ReentrantLock();

        public void lockDeduct() {
            try {
                lock.lock();
                int count = prize.getCount();
                if (count > 0) {
                    prize.setCount(count - 1);
                }
            } finally {
                lock.unlock();
            }
        }
    }

    @GroupThreads(10)
    @Group("sync")
    @Benchmark
    public void syncDeduct(MonitorSync monitorSync) {
        monitorSync.syncDeduct();
    }

    @GroupThreads(10)
    @Group("reference")
    @Benchmark
    public void referenceDeduct(MonitorReference monitorReference) {
        monitorReference.referenceDeduct();
    }

    @GroupThreads(10)
    @Group("lock")
    @Benchmark
    public void lockDeduct(MonitorLock monitorLock) {
        monitorLock.lockDeduct();
    }

    public static void main(String[] args) throws Exception {
        Options options = new OptionsBuilder().include(SynchronizedVsAtomicReference.class.getSimpleName())
                .addProfiler(StackProfiler.class)
                .build();
        new Runner(options).run();
    }
}

执行结果如下:

Benchmark                                       Mode  Cnt  Score   Error  Units
SynchronizedVsAtomicReference.lock              avgt   10  0.251 ± 0.020  us/op
SynchronizedVsAtomicReference.reference         avgt   10  0.328 ± 0.022  us/op
SynchronizedVsAtomicReference.sync              avgt   10  0.959 ± 0.285  us/op

线程状态数据统计结果如下:

SynchronizedVsAtomicReference.lock:·stack:
 78.1%         WAITING
 21.9%         RUNNABLE
   
SynchronizedVsAtomicReference.reference:·stack:
 98.7%         RUNNABLE
  1.3%         WAITING

SynchronizedVsAtomicReference.sync:·stack:
 79.8%         BLOCKED
 19.3%         RUNNABLE
  0.8%         WAITING

以上数据可以看出,针对我的测试代码来讲,性能结果为:

显示锁Lock > AtomicReference > synchronized

但是从线程状态统计数据对比来看,AtomicReference线程的RUNNABLE状态高达98.7%,而Lock线程的RUNNABLE状态仅仅只有21.9%;观察代码发现,我得出的结果应该是referenceDeduct方法中创建对象的过程增大了整体执行时间;我觉得Atomic的Lock-Free设计,性能还是非常不错的,所以我们在进行线程安全处理的时候要充分考虑当前的使用场景,根据不同的场景选择性能比较优异的解决方案。


AtomicStampedReference

到这里我们已经学习了AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference这些原子类型,它们都采用了基于volatile关键字+CAS算法无锁化的操作方式来确保共享数据在多线程下的线程安全。

CAS算法大家肯定不陌生,通俗来说就是先比较,再更新(交换)。

但是假设线程1将一个变量A变成了B,紧接着又从B变成了A(完璧归赵);对于线程2来说,拿到的数据虽然是A,但自己不知道该数据已经被修改过了(A已经脏了,对于有洁癖地线程1来说是不能忍的),这就是CAS算法的ABA问题,你在面试中有被面试官问过吗?

这一小节的主角AtomicStampedReference就是来解决ABA问题的;在数据库操作的过程中,我们也曾使用过乐观锁版本号对其进行ABA问题的解决;AtomicStampedReference也是通过增加版本号的方式:

看看构造函数:

public AtomicStampedReference(V initialRef, int initialStamp) {
  pair = Pair.of(initialRef, initialStamp);
}

构造函数中initialStamp属性就是一个需要维护的版本号;

特别注意:此版本号需要应用程序自身去负责,AtomicStampedReference并没有提供安全性操作。

其API相对简单,大家可以去尝试着用用。


AtomicArray

该类提供了对数组数据类型的原子操作: AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

简单测试一下:

@Test
public void addTest() {
  int[] intArray = {1, 2, 3, 4, 5, 6};
  AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(intArray);

  //对索引为2的元素增加10
  assert atomicIntegerArray.addAndGet(2, 10) == 13;
  //获得索引为2的元素
  assert atomicIntegerArray.get(2) == 13;
}

该类可以对某个索引的操作是原子性的


AtomicFieldUpdater

该类提供了对象属性的原子性更新的操作

public class AtomicFieldUpdaterTest {

    @Data
    public static class User {
        private String name;

        volatile int money;

        public User() {
        }

        public User(String name, int money) {
            this.name = name;
            this.money = money;
        }
    }

    @Test
    public void addTest() {
        AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "money");
        User user = new User("张三", 100);
        assert 120 == updater.addAndGet(user, 20);
    }
}

我们这做了一个简单测试,AtomicIntegerFieldUpdater.newUpdater(User.class, "money");传入需要进行原子操作的类"User.class"和需要原子操作的字段"money",然后调用updater的API,所对"money"字段的操作均是原子性的。

特别注意的是,要满足原子操作的属性要求还很高:

  1. 字段没有被volatile修饰无法被原子性地更新(volatile修饰后线程可见)
  2. 类变量无法被原子性地更新(即字段不能被static修饰)
  3. 无法直接访问的成员变量属性不能被原子性地更新(即字段不能被private修饰)
  4. final修饰的字段不能被原子性地更新
  5. 父类的成员属性无法被原子性地更新

我们的原子类型就学习到这里,相信大家肯定对JAVA的原子类型有了进一步的认识;

那我们下一个体系学习JAVA并发工具类,让我们更进一步的走进并发编程。

公众号传送门——《并发编程之AtomicReference》

码云代码链接如下:

gitee.com/songyanzhi/…

感谢阅读,祝大家工作愉快、身体健康!