AtomicReference 原理


轉載自:https://segmentfault.com/a/1190000015831791?utm_source=tag-newest#articleHeader0

就是以原子方式更新對象引用。

可以看到它持有一個對象的引用,-value,用volatile修飾,並通過unsafe類來操作該引用。

 1 private static final Unsafe unsafe = Unsafe.getUnsafe();
 2     private static final long valueOffset;
 3 
 4     static {
 5       try {
 6         valueOffset = unsafe.objectFieldOffset
 7             (AtomicReference.class.getDeclaredField("value"));
 8       } catch (Exception ex) { throw new Error(ex); }
 9     }
10 
11     private volatile V value;
12 
13     /**
14      * Creates a new AtomicReference with the given initial value.
15      *
16      * @param initialValue the initial value
17      */
18     public AtomicReference(V initialValue) {
19         value = initialValue;
20     }
21 
22     /**
23      * Creates a new AtomicReference with null initial value.
24      */
25     public AtomicReference() {
26     }

為什么需要AtomicReference?難道多個線程同時對一個引用變量賦值也會出現並發問題?
引用變量的賦值本身沒有並發問題,也就是說對於引用變量var ,類似下面的賦值操作本身就是原子操作:
Foo var = ... ;
AtomicReference的引入是為了可以用一種類似樂觀鎖的方式操作共享資源,在某些情景下以提升性能。

我們知道,當多個線程同時訪問共享資源時,一般需要以加鎖的方式控制並發:

volatile Foo sharedValue = value; Lock lock = new ReentrantLock(); lock.lock(); try{ // 操作共享資源sharedValue } finally{ lock.unlock(); }

上述訪問方式其實是一種對共享資源加悲觀鎖的訪問方式。

而AtomicReference提供了以無鎖方式訪問共享資源的能力,看看如何通過AtomicReference保證線程安全,來看個具體的例子:

 1 package com.citi.test.mutiplethread.demo0503;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import java.util.concurrent.atomic.AtomicReference;
 6 
 7 public class AtomicReferenceTest {
 8     public static void main(String[] args) throws InterruptedException {
 9         AtomicReference<Integer> ref=new AtomicReference<Integer>(new Integer(1000));
10         List<Thread> list=new ArrayList<Thread>();
11         for(int i=0;i<1000;i++){
12             Thread t=new Thread(new Task(ref),"Thread-"+i);
13             list.add(t);
14             t.start();
15         }
16         for(Thread t: list){
17             System.out.println(t.getName());
18             t.join();
19         }
20         System.out.println(ref.get());
21     }
22 }
23 class Task implements Runnable{
24     private AtomicReference<Integer> ref;
25     public Task(AtomicReference<Integer> ref) {
26         this.ref=ref;
27     }
28     @Override
29     public void run() {
30         for(;;){
31             Integer oldV=ref.get();
32             System.out.println(Thread.currentThread().getName()+":"+oldV);
33             if(ref.compareAndSet(oldV, oldV+1)){
34                 break;
35             }
36         }
37     }
38 }
View Code

上述示例,最終打印“2000”。

該示例並沒有使用鎖,而是使用自旋+CAS的無鎖操作保證共享變量的線程安全。1000個線程,每個線程對金額增加1,最終結果為2000,如果線程不安全,最終結果應該會小於2000。

通過示例,可以總結出AtomicReference的一般使用模式如下:

AtomicReference<Object> ref = new AtomicReference<>(new Object()); Object oldCache = ref.get(); // 對緩存oldCache做一些操作 Object newCache = someFunctionOfOld(oldCache); // 如果期間沒有其它線程改變了緩存值,則更新 boolean success = ref.compareAndSet(oldCache , newCache);

上面的代碼模板就是AtomicReference的常見使用方式,看下compareAndSet方法:

clipboard.png

該方法會將入參的expect變量所指向的對象和AtomicReference中的引用對象進行比較,如果兩者指向同一個對象,則將AtomicReference中的引用對象重新置為update,修改成功返回true,失敗則返回false。也就是說,AtomicReference其實是比較對象的引用

CAS可能存在ABA的問題。

CAS操作可能存在 ABA的問題,就是說:
假如一個值原來是A,變成了B,又變成了A,那么CAS檢查時會發現它的值沒有發生變化,但是實際上卻變化了。

一般來講這並不是什么問題,比如數值運算,線程其實根本不關心變量中途如何變化,只要最終的狀態和預期值一樣即可。

但是,有些操作會依賴於對象的變化過程,此時的解決思路一般就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那么A-B-A 就會變成1A - 2B - 3A。

四、AtomicStampedReference的引入

AtomicStampedReference就是上面所說的加了版本號的AtomicReference。

AtomicStampedReference原理

先來看下如何構造一個AtomicStampedReference對象,AtomicStampedReference只有一個構造器:

 

 

可以看到,除了傳入一個初始的引用變量initialRef外,還有一個initialStamp變量,initialStamp其實就是版本號(或者說時間戳),用來唯一標識引用變量。

在構造器內部,實例化了一個Pair對象,Pair對象記錄了對象引用和時間戳信息,采用int作為時間戳,實際使用的時候,要保證時間戳唯一(一般做成自增的),如果時間戳如果重復,還會出現ABA的問題。

AtomicStampedReference的所有方法,其實就是Unsafe類針對這個 Pair對象的操作。
和AtomicReference相比,AtomicStampedReference中的每個引用變量都帶上了pair.stamp這個版本號,這樣就可以解決CAS中的ABA問題了。

AtomicStampedReference使用示例

來看下AtomicStampedReference的使用:

AtomicStampedReference<Foo>  asr = new AtomicStampedReference<>(null,0); // 創建AtomicStampedReference對象,持有Foo對象的引用,初始為null,版本為0 int[] stamp=new int[1]; Foo oldRef = asr.get(stamp); // 調用get方法獲取引用對象和對應的版本號 int oldStamp=stamp[0]; // stamp[0]保存版本號 asr.compareAndSet(oldRef, null, oldStamp, oldStamp + 1) //嘗試以CAS方式更新引用對象,並將版本號+1

上述模板就是AtomicStampedReference的一般使用方式,注意下compareAndSet方法:

 

 

我們知道,AtomicStampedReference內部保存了一個pair對象,該方法的邏輯如下:

  1. 如果AtomicStampedReference內部pair的引用變量、時間戳 與 入參expectedReferenceexpectedStamp都一樣,說明期間沒有其它線程修改過AtomicStampedReference,可以進行修改。此時,會創建一個新的Pair對象(casPair方法,因為Pair是Immutable類)。

但這里有段優化邏輯,就是如果 newReference == current.reference && newStamp == current.stamp,說明用戶修改的新值和AtomicStampedReference中目前持有的值完全一致,那么其實不需要修改,直接返回true即可。

四、AtomicMarkableReference

我們在講ABA問題的時候,引入了AtomicStampedReference。

AtomicStampedReference可以給引用加上版本號,追蹤引用的整個變化過程,如:
A -> B -> C -> D - > A,通過AtomicStampedReference,我們可以知道,引用變量中途被更改了3次。

但是,有時候,我們並不關心引用變量更改了幾次,只是單純的關心是否更改過,所以就有了AtomicMarkableReference

可以看到,AtomicMarkableReference的唯一區別就是不再用int標識引用,而是使用boolean變量——表示引用變量是否被更改過

從語義上講,AtomicMarkableReference對於那些不關心引用變化過程,只關心引用變量是否變化過的應用會更加友好。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM