并发编程原子类完整教程: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)));
}
评论区