本文首發於一世流雲的專欄: https://segmentfault.com/blog...
AtomicReference,顧名思義,就是以原子方式更新對象引用。java
能夠看到,AtomicReference持有一個對象的引用——value,並經過Unsafe類來操做該引用:segmentfault
爲何須要AtomicReference?難道多個線程同時對一個引用變量賦值也會出現併發問題?
引用變量的賦值自己沒有併發問題,也就是說對於引用變量var ,相似下面的賦值操做自己就是原子操做:
Foo var = ... ;
AtomicReference的引入是爲了能夠用一種相似樂觀鎖的方式操做共享資源,在某些情景下以提高性能。
咱們知道,當多個線程同時訪問共享資源時,通常須要以加鎖的方式控制併發:緩存
volatile Foo sharedValue = value; Lock lock = new ReentrantLock(); lock.lock(); try{ // 操做共享資源sharedValue } finally{ lock.unlock(); }
上述訪問方式實際上是一種對共享資源加悲觀鎖的訪問方式。安全
而AtomicReference提供了以無鎖方式訪問共享資源的能力,看看如何經過AtomicReference保證線程安全,來看個具體的例子:併發
public class AtomicRefTest { public static void main(String[] args) throws InterruptedException { AtomicReference<Integer> ref = new AtomicReference<>(new Integer(1000)); List<Thread> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { Thread t = new Thread(new Task(ref), "Thread-" + i); list.add(t); t.start(); } for (Thread t : list) { t.join(); } System.out.println(ref.get()); // 打印2000 } } class Task implements Runnable { private AtomicReference<Integer> ref; Task(AtomicReference<Integer> ref) { this.ref = ref; } @Override public void run() { for (; ; ) { //自旋操做 Integer oldV = ref.get(); if (ref.compareAndSet(oldV, oldV + 1)) // CAS操做 break; } } }
上述示例,最終打印「2000」。ide
該示例並無使用鎖,而是使用自旋+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方法:this
該方法會將入參的expect變量所指向的對象和AtomicReference中的引用對象進行比較,若是二者指向同一個對象,則將AtomicReference中的引用對象從新置爲update,修改爲功返回true,失敗則返回false。也就是說,AtomicReference實際上是比較對象的引用。spa
CAS操做可能存在 ABA的問題,就是說:
假如一個值原來是A,變成了B,又變成了A,那麼CAS檢查時會發現它的值沒有發生變化,可是實際上卻變化了。
通常來說這並非什麼問題,好比數值運算,線程其實根本不關心變量中途如何變化,只要最終的狀態和預期值同樣便可。
可是,有些操做會依賴於對象的變化過程,此時的解決思路通常就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A - 2B - 3A。
AtomicStampedReference就是上面所說的加了版本號的AtomicReference。
先來看下如何構造一個AtomicStampedReference對象,AtomicStampedReference只有一個構造器:
能夠看到,除了傳入一個初始的引用變量initialRef外,還有一個initialStamp變量,initialStamp其實就是版本號(或者說時間戳),用來惟一標識引用變量。
在構造器內部,實例化了一個Pair對象,Pair對象記錄了對象引用和時間戳信息,採用int做爲時間戳,實際使用的時候,要保證時間戳惟一(通常作成自增的),若是時間戳若是重複,還會出現ABA的問題。
AtomicStampedReference的全部方法,其實就是Unsafe類針對這個 Pair對象的操做。
和AtomicReference相比,AtomicStampedReference中的每一個引用變量都帶上了pair.stamp這個版本號,這樣就能夠解決CAS中的ABA問題了。
來看下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對象,該方法的邏輯以下:
但這裏有段優化邏輯,就是若是 newReference == current.reference && newStamp == current.stamp
,說明用戶修改的新值和AtomicStampedReference中目前持有的值徹底一致,那麼其實不須要修改,直接返回true便可。
咱們在講ABA問題的時候,引入了AtomicStampedReference。
AtomicStampedReference能夠給引用加上版本號,追蹤引用的整個變化過程,如:
A -> B -> C -> D - > A,經過AtomicStampedReference,咱們能夠知道,引用變量中途被更改了3次。
可是,有時候,咱們並不關心引用變量更改了幾回,只是單純的關心是否更改過,因此就有了AtomicMarkableReference:
能夠看到,AtomicMarkableReference的惟一區別就是再也不用int標識引用,而是使用boolean變量——表示引用變量是否被更改過。
從語義上講,AtomicMarkableReference對於那些不關心引用變化過程,只關心引用變量是否變化過的應用會更加友好。