AtomicStampedReference是java并发包下提供的一个原子类,它能解决其它原子类无法解决的ABA问题
,比如AtomicInteger存在ABA问题
ABA问题发生在多线程环境中,当某线程连续读取同一块内存地址两次,两次得到的值一样,它简单地认为“此内存地址的值并没有被修改过”,然而,同时可能存在另一个线程在这两次读取之间把这个内存地址的值从A修改成了B又修改回了A,这时还简单地认为“没有修改过”显然是错误的。
比如,两个线程按下面的顺序执行:
(1)线程1读取内存位置X的值为A;
(2)线程1阻塞了;
(3)线程2读取内存位置X的值为A;
(4)线程2修改内存位置X的值为B;
(5)线程2修改又内存位置X的值为A;
(6)线程1恢复,继续执行,比较发现还是A把内存位置X的值设置为C;
可以看到,针对线程1来说,第一次的A和第二次的A实际上并不是同一个A。
ABA问题通常发生在无锁结构中,用代码来表示上面的过程大概就是这样:
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(1);
new Thread(new Runnable() {
@Override
public void run() {
int value = atomicInteger.get();
System.out.println("线程1读到value="+value);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (atomicInteger.compareAndSet(value,3)) {
System.out.println("线程1修改value从"+value+"到3");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int value = atomicInteger.get();
System.out.println("线程2读到value="+value);
if (atomicInteger.compareAndSet(value,2)) {
System.out.println("线程2修改value从"+value+"到2");
value = atomicInteger.get();
System.out.println("线程2读到value="+value);
if (atomicInteger.compareAndSet(value,1)){
System.out.println("线程2修改value从"+value+"到1");
}
}
}
}).start();
}
输出
线程1读到value=1
线程2读到value=1
线程2修改value从1到2
线程2读到value=2
线程2修改value从2到1
线程1修改value从1到3
AtomicStampedReference
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);
}
}
将元素值和版本号绑定在一起,存储在Pair的reference和stamp(邮票、戳的意思)中。
public AtomicStampedReference(V initialRef, int initialStamp) {
// 也就是初始化的时候必须传入元素值和版本号
pair = Pair.of(initialRef, initialStamp);
}
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
// 获取当前的(元素值和版本号)对
Pair<V> current = pair;
return
// 元素没变
expectedReference == current.reference &&
// 版本号没变
expectedStamp == current.stamp &&
// 新引用等于旧引用
((newReference == current.reference &&
// 新版本号等于旧版本号
newStamp == current.stamp) ||
// 构造新的Pair对象并CAS更新
casPair(current, Pair.of(newReference, newStamp)));
}
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
private boolean casPair(Pair<V> cmp, Pair<V> val) {
// 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
(1)如果元素值和版本号都没有变化,并且和新的也相同,返回true;
(2)如果元素值和版本号都没有变化,并且和新的不完全相同,就构造一个新的Pair对象并执行CAS更新pair。
使用AtomicStampedReference解决那个AtomicInteger带来的ABA问题
public static void main(String[] args) {
AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(1,1);
int[] stampHolder = new int[1];
new Thread(new Runnable() {
@Override
public void run() {
int value = atomicInteger.get(stampHolder);
// 一定要先保存原来的值
int stamp = stampHolder[0];
System.out.println("线程1读到value="+value+"、stamp="+stampHolder[0]);
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1读到value="+value+"、stamp="+stampHolder[0]);
// 这里变成stamp是为了传入之前的值,如果是stampHolder[0]则是最新的值也就没有意义了
if (atomicInteger.compareAndSet(value,3,stamp,stampHolder[0]+1)) {
System.out.println("线程1修改value从"+value+"到3、"+"stamp从"+stampHolder[0]+"到"+(stampHolder[0]+1));
}else{
System.out.println("线程1修改失败");
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
int value = atomicInteger.get(stampHolder);
System.out.println("线程2读到value="+value+"、stamp="+stampHolder[0]);
if (atomicInteger.compareAndSet(value,2,stampHolder[0],stampHolder[0]+1)) {
System.out.println("线程2修改value从"+value+"到2、"+"stamp从"+stampHolder[0]+"到"+(stampHolder[0]+1));
value = atomicInteger.get(stampHolder);
System.out.println("线程2读到value="+value+"、stamp="+stampHolder[0]);
if (atomicInteger.compareAndSet(value,1,stampHolder[0],stampHolder[0]+1)){
System.out.println("线程2修改value从"+value+"到1、"+"stamp从"+stampHolder[0]+"到"+(stampHolder[0]+1));
value = atomicInteger.get(stampHolder);
System.out.println("线程2读到value="+value+"、stamp="+stampHolder[0]);
}
}
}
}).start();
}
可以看到线程1最后更新1到3时失败了,因为这时版本号也变了,成功解决了ABA的问题。
线程2读到value=1、stamp=1
线程1读到value=1、stamp=1
线程2修改value从1到2、stamp从1到2
线程2读到value=2、stamp=2
线程2修改value从2到1、stamp从2到3
线程2读到value=1、stamp=3
线程1读到value=1、stamp=3
线程1修改失败
(1)在多线程环境下使用无锁结构要注意ABA问题;
(2)ABA的解决一般使用版本号来控制
,并保证数据结构使用元素值来传递,且每次添加元素都新建节点承载元素值;
(3)AtomicStampedReference内部使用Pair来存储元素值及其版本号;