简介
今天我们来学习另外几个开发过程中可能会用到的原子类,或者在面试的时候会被问到的类。
分别是:
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"字段的操作均是原子性的。
特别注意的是,要满足原子操作的属性要求还很高:
- 字段没有被volatile修饰无法被原子性地更新(volatile修饰后线程可见)
- 类变量无法被原子性地更新(即字段不能被static修饰)
- 无法直接访问的成员变量属性不能被原子性地更新(即字段不能被private修饰)
- final修饰的字段不能被原子性地更新
- 父类的成员属性无法被原子性地更新
我们的原子类型就学习到这里,相信大家肯定对JAVA的原子类型有了进一步的认识;
那我们下一个体系学习JAVA并发工具类,让我们更进一步的走进并发编程。
公众号传送门——《并发编程之AtomicReference》
码云代码链接如下:
感谢阅读,祝大家工作愉快、身体健康!