简单介绍
之前我们介绍了AtomicInteger的源码,相当于是对Integer的一些封装,其内部使用unsafe执行原子化操作。在这个基础之上提供了+1,-1的针对数字的操作。那么针对一般化对象呢?这个时候我们就能用上AtomicReference作为一般引用的原子操作类了。这个类提供的cas操作对比的是对象的引用。我们把常用的方法拉出来看看:
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
你会发现,和我们之前接触的AtomicInteger没什么大的差别。因此AtomicReference相关的源码解析我们就忽略了。但是除此之外我们残留一个问题并没有解决,也就是ABA问题。
ABA问题,简单的说就是一个线程将引用从A变更到B再变更到A,但是另一个线程并没有感知到这个过程。一般情况下没啥问题,就怕A-B-A的过程中把A内的数据给变更了这将会导致数据错乱。举个例子:A-B-C的链表,Thread1干的事儿是将取出A,将B删除,Thread2将A替换为D,这个时候就有可能变成D-B-C而不是D-C。
为了解决上述的问题,JDK也有现成的方案,一个是AtomicMarkableReference�,另一个是AtomicStampedReference。AtomicMarkableReference使用布尔值来区别新旧值,而AtomicStampedReference则是使用版本号来控制新旧。
AtomicMarkableReference
AtomicMarkableReference使用一个布尔值来控制是否是旧值,如下存在一个pair的内部类,将我们的引用与布尔值进行关联存储。
private volatile Pair<V> pair;
private static class Pair<T> {
final T reference;
final boolean mark;
private Pair(T reference, boolean mark) {
this.reference = reference;
this.mark = mark;
}
static <T> Pair<T> of(T reference, boolean mark) {
return new Pair<T>(reference, mark);
}
}
AtomicMarkableReference存在pair的字段存储数据,可以利用以下几个方法获得pair中的数据:
public V getReference() {
return pair.reference;
}
public boolean isMarked() {
return pair.mark;
}
如此一来,我们在使用AtomicMarkableReference进行cas的时候,就需要同时传递mark值进行判断:
// 原子操作,expectedReference,expectedMark用来匹配是否满足cas的条件
public boolean compareAndSet(V expectedReference,
V newReference,
boolean expectedMark,
boolean newMark) {
// 读取最新数据
Pair<V> current = pair;
return
// 引用相等
expectedReference == current.reference &&
// 标记相等
expectedMark == current.mark &&
((newReference == current.reference &&
newMark == current.mark) ||
// 替换pair
casPair(current, Pair.of(newReference, newMark)));
}
// set方法,注意,并非是原子操作
public void set(V newReference, boolean newMark) {
Pair<V> current = pair;
if (newReference != current.reference || newMark != current.mark)
this.pair = Pair.of(newReference, newMark);
}
// expectedReference匹配的时候,更新标记
public boolean attemptMark(V expectedReference, boolean newMark) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
(newMark == current.mark ||
casPair(current, Pair.of(expectedReference, newMark)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
AtomicStampedReference�
AtomicStampedReference解决ABA问题更加常态化,其内部使用版本号进行控制。毕竟AtomicMarkableReference仅仅存在true/false两种状态。类似于AtomicMarkableReference,当前类也是使用Pair封装了引用与版本号:
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;
因此我们在更新数据的时候,必须使用引用与版本号进行同时匹配才能进行cas:
// 当expectedReference,expectedStamp匹配的时候,更新newReference,newStamp
// 如果更新成功则返回true
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)));
}
// 注意这里没有原子操
public void set(V newReference, int newStamp) {
Pair<V> current = pair;
if (newReference != current.reference || newStamp != current.stamp)
this.pair = Pair.of(newReference, newStamp);
}
// expectedReference匹配的场合下,更新版本号
public boolean attemptStamp(V expectedReference, int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
(newStamp == current.stamp ||
casPair(current, Pair.of(expectedReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
总结
AtomicInteger是数值对象的线程安全封装,将数值对象扩充到一般对象就可以使用AtomicReference来搞定,但是需要注意的是,使用cas处理并发问题有可能会导致ABA问题,JDK内自带两个解决这个问题的类,其本质思路都是使用版本进行控制。其中AtomicMarkableReference使用的是布尔值来控制,AtomicStampedReference则更加宽泛,使用数字作为版本号,每次进行cas更新的时候,都需要使用既有的版本号与引用进行匹配,匹配成功之后才能执行更新操作