一、前言
在《并发编程学习笔记 之 原子操作类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