同步数据结构之原子复合类

引言

前面介绍的原子更新器都只是针对单个对象或者字段元素进行原子更新操作,在序章中提到的第四类复合型的原子更新器可以将一个引用变量与其他变量绑定到一起,实现复合的原子更新操作,这类更新器有两个: AtomicMarkableReference、AtomicStampedReference。

 

AtomicStampedReference

AtomicStampedReference是为了解决CAS操作过程中的ABA问题,它将一个引用类似和一个int型的标识版本号的变量作为二元组合整体,所以AtomicStampedReference也被称为带版本戳的原子引用类型。每一次试图原子更新引用类型的时候都至少需要比较其携带的版本号,如果版本号不一致将会更新失败,当然更新的时候对应的版本号也会一起被更新。还是以源码开始进行分析吧。

 1 public class AtomicStampedReference<V> {  
 2     //静态内部类Pair将对应的引用类型和版本号stamp作为它的成员  
 3     private static class Pair<T> {  
 4         final T reference;  
 5         final int stamp;  
 6         private Pair(T reference, int stamp) {  
 7             this.reference = reference;  
 8             this.stamp = stamp;  
 9         }  
10         static <T> Pair<T> of(T reference, int stamp) {  
11             return new Pair<T>(reference, stamp);  
12         }  
13     }  
14       
15     //作为一个整体的pair变量被volatile修饰  
16     private volatile Pair<V> pair;  
17   
18     //构造方法,参数分别是初始引用变量的值和初始版本号  
19     public AtomicStampedReference(V initialRef, int initialStamp) {  
20         pair = Pair.of(initialRef, initialStamp);  
21     }  
22       
23     ....  
24       
25     private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();  
26     private static final long pairOffset =  
27         objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);  
28   
29     //获取pair成员的偏移地址  
30     static long objectFieldOffset(sun.misc.Unsafe UNSAFE,  
31                                   String field, Class<?> klazz) {  
32         try {  
33             return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));  
34         } catch (NoSuchFieldException e) {  
35             NoSuchFieldError error = new NoSuchFieldError(field);  
36             error.initCause(e);  
37             throw error;  
38         }  
39     }  
40 }  

 

由以上的源码可见, AtomicStampedReference内部有一个静态内部类Pair的实例变量pair来存放初始化AtomicStampedReference时传入的初始引用变量和初始版本号,注意它是被volatile修饰的,保证了其可见性;并且在每一次原子更新操作的时候都会生成新的pair实例替代旧的pair,即把真正的引用变量和版本号作为一个整体进行更新,所以在AtomicStampedReference内部还提前获取了该pair变量的偏移地址,为原子更新操作作准备。

 

接下来,看看它的方法。它的方法分为两类,一类是get方法,另一类就是原子更新方法:

 1 //获取现在的引用类型变量的值  
 2 public V getReference() {  
 3     return pair.reference;  
 4 }  
 5 //获取现在的版本号  
 6 public int getStamp() {  
 7     return pair.stamp;  
 8 }  
 9 //同时获取版本号和引用变量,参数必须是一个长度的int数组,  
10 //因为int型的版本号不能通过引用传递,所以通过参数指定的数组进行返回。  
11 //而对于的引用变量则直接返回。  
12 public V get(int[] stampHolder) {  
13     Pair<V> pair = this.pair;  
14     stampHolder[0] = pair.stamp;  
15     return pair.reference;  
16 }  

然后就是最重要的原子更新操作相关的方法:

 1 //同时原子的更新对于的引用变量和版本号,当引用类型和版本号都和期望的值一致的时候。成功返回true  
 2 //根据原子类序章,该方法只保证对自己所操作的变量的原子性和可见性,没有其他的happen-before规则成立。  
 3 //这里的源码虽然看起来是直接调用了compareAndSet,但是不保证运行时不被JVM替换掉真正的实现方法。  
 4 public boolean weakCompareAndSet(V   expectedReference,  
 5                                  V   newReference,  
 6                                  int expectedStamp,  
 7                                  int newStamp) {  
 8     return compareAndSet(expectedReference, newReference,  
 9                          expectedStamp, newStamp);  
10 }  
11 //CAS原子更新操作,当引用类型和版本号都和预期的一致的时候进行原子更新,由于它是直接更新整个volatile修饰的pair变量,  
12 所以满足可见性,有序性。成功返回true  
13 public boolean compareAndSet(V   expectedReference,  
14                              V   newReference,  
15                              int expectedStamp,  
16                              int newStamp) {  
17     Pair<V> current = pair;  
18     return  
19         expectedReference == current.reference &&  
20         expectedStamp == current.stamp &&  
21         ((newReference == current.reference &&  
22           newStamp == current.stamp) ||  
23          casPair(current, Pair.of(newReference, newStamp)));  
24 }  
25   
26 //直接将原来的引用类型变量和版本号更改为新的值。  
27 public void set(V newReference, int newStamp) {  
28     Pair<V> current = pair;  
29     if (newReference != current.reference || newStamp != current.stamp)  
30         this.pair = Pair.of(newReference, newStamp);  
31 }  
32 //当引用类型的值与期望的一致的时候,原子的更改版本号为新的值。该方法只修改版本号,不修改引用变量的值,成功返回true  
33 public boolean attemptStamp(V expectedReference, int newStamp) {  
34     Pair<V> current = pair;  
35     return  
36         expectedReference == current.reference &&  
37         (newStamp == current.stamp ||  
38          casPair(current, Pair.of(expectedReference, newStamp)));  
39 }  
40 //该方法是其他CAS方法的真正实现方法,其实就是通过unsafe的CAS方法将pair原子的更改为新的值。成功返回true  
41 private boolean casPair(Pair<V> cmp, Pair<V> val) {  
42     return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);  
43 } 

通过以上原子更新方法,可见 AtomicStampedReference就是利用了Unsafe的CAS方法+Volatile关键字对存储实际的引用变量和int的版本号的Pair实例进行更新。

 

AtomicMarkableReference

AtomicMarkableReference其实可以看作是 AtomicStampedReference的特例,因为AtomicMarkableReference是将一个引用变量和一个布尔变量作为一个二元组合整体,它的版本号相当于只有true和false,而不像AtomicStampedReference那样可以对每一次的更新都记录版本号,因为在有些场合我们只需要知道是否有人等我们关注的对象进行了更改,如果已经更改就不再作任何更改,例如对节点删除的标记,这是其实通过AtomicMarkableReference就能够达到我们的要求。

 

关于AtomicMarkableReference的源码,其实它和AtomicStampedReference的设计原理是一样的,只是将标识版本号的int型变量换成了布尔型的变量,还是以一个静态内部类Pair将这个引用变量和布尔变量组合在一起进行原子的更新。所以其源码就不再一一列举。

 1 public class AtomicMarkableReferenceTest {  
 2   
 3     public static void main(String[] args) {  
 4         AtomicMarkableReference<String> amr = new AtomicMarkableReference<String>("ABC", false);  
 5           
 6         boolean result = amr.attemptMark("ABC", true);//标记为true  
 7           
 8         System.out.println(result);  
 9           
10         System.out.println(amr.isMarked());//返回是否被标记了,true  
11           
12         AtomicStampedReference<String>  asr = new AtomicStampedReference<String>("123", 0);  
13           
14         boolean seted = asr.compareAndSet("123", "456", 0, 1);//原子的更新原值和版本号  
15         System.out.println(seted);  
16           
17         System.out.println(asr.getStamp());//获取新的版本号 1  
18     }  
19   
20 }  

 

posted @ 2021-05-11 16:58  莫待樱开春来踏雪觅芳踪  阅读(75)  评论(0)    收藏  举报