AtomicStampedReference
- AtomicStampedReference的应用场景是什么?
- AtomicStampedReference如何解决 ABA 问题?
AtomicStampedReference的应用场景是什么?
@Test
public void test2() throws InterruptedException {
AtomicInteger value = new AtomicInteger(1);
Thread t1 = new Thread(() -> {
System.out.println("Thread 1: Value = " + value.get()); // 输出1
value.compareAndSet(1, 2);
System.out.println("Thread 1: First Value = " + value.get()); // 输出2
System.out.println("Do Something");
value.compareAndSet(2, 1);
System.out.println("Thread 1: Second Value = " + value.get()); // 输出1
});
Thread t2 = new Thread(() -> {
System.out.println("Thread 2: Value = " + value.get()); // 输出1
value.compareAndSet(1, 3);
System.out.println("Thread 2: Value = " + value.get()); // 输出3
});
t1.start();
Thread.sleep(100);
t2.start();
}
在 t2 中无法辨别 value 是走了 t1 线程还是原数据,因此这就产生了 ABA 问题。而采用 AtomicStampedReference 就可以很好的解决该问题
@Test
public void test() throws InterruptedException {
AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<>(1, 0);
new Thread(() -> {
System.out.println("Thread 1: Value = " + atomicStampedRef.getReference() + ", Stamp = " + stamp);
atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1);
System.out.println("Thread 1 First: Value = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp()); // First: Value = 2, Stamp = 1
int newStamp = atomicStampedRef.getStamp();
atomicStampedRef.compareAndSet(2, 1, newStamp, newStamp + 1);
System.out.println("Thread 1 Second: Value = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp());// Second: Value = 1, Stamp = 2
}).start();
Thread.sleep(100);
new Thread(() -> {
System.out.println("Thread 2: Value = " + atomicStampedRef.getReference() + ", Stamp = " + stamp);
atomicStampedRef.compareAndSet(1, 3, stamp, stamp + 1);
System.out.println("Thread 2 Modify: Value = " + atomicStampedRef.getReference() + ", Stamp = " + atomicStampedRef.getStamp()); // Modify: Value = 1, Stamp = 2
}).start();
}
AtomicStampedReference如何解决 ABA 问题?
CAS有ABA问题的且ABA问题是针对引用型对象的,而 AtomicStampedReference 就是为了解决这一问题。 MySQL 的乐观锁通过加版本号来解决 ABA 问题,因此在 Java 中我们也可以借鉴 MySQL 的思想解决 CAS 的 ABA 问题
public class AtomicStampedReference<V> {
// 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);
}
}
private volatile Pair<V> pair; // 通过 volatile 修饰在多线程情况下可以发现 pair 的变化
}
下面的代码展示了具体如何解决 CAS 中的 ABA 问题
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) ||
casPair(current, Pair.of(newReference, newStamp))); // 新创建对象,不是修改对象属性
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
只有在 旧引用相同并且旧版本相同的情况下,才能进行修改。如果新版本的引用和时间戳一样则不需要进行修改,否则新创建一个 Pair 对象
在没看源码之前,我以为会通过 CAS 修改 Pair 的两个属性去达到最终一致性,但是源码是新创建一个对象。
我猜想可能是因为修改两个属性的话,CAS 不好操作,CAS 修改单个变量比较好操作,而同时对两个变量不是很好操作,如果想要保证同时对两个变量进行操作需要加锁,反而让问题变得更加复杂,不如新创建一个对象。而新创建一个对象通过 CAS 判断就比较简单,就相当于只对单个变量进行 CAS 操作