Java多線程進階(十四)—— J.U.C之atomic框架:AtomicReference

timg.jpeg

本文首發於一世流雲的專欄: https://segmentfault.com/blog...

1、AtomicReference簡介

AtomicReference,顧名思義,就是以原子方式更新對象引用。java

能夠看到,AtomicReference持有一個對象的引用——value,並經過Unsafe類來操做該引用:segmentfault

clipboard.png

爲何須要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

clipboard.png

該方法會將入參的expect變量所指向的對象和AtomicReference中的引用對象進行比較,若是二者指向同一個對象,則將AtomicReference中的引用對象從新置爲update,修改爲功返回true,失敗則返回false。也就是說,AtomicReference實際上是比較對象的引用spa

2、AtomicReference接口/類聲明

類聲明

clipboard.png

接口聲明

clipboard.png

3、CAS操做可能存在的問題

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

通常來說這並非什麼問題,好比數值運算,線程其實根本不關心變量中途如何變化,只要最終的狀態和預期值同樣便可。

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

4、AtomicStampedReference的引入

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

AtomicStampedReference原理

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

clipboard.png

能夠看到,除了傳入一個初始的引用變量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方法:

clipboard.png

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

  1. 若是AtomicStampedReference內部pair的引用變量、時間戳 與 入參expectedReferenceexpectedStamp都同樣,說明期間沒有其它線程修改過AtomicStampedReference,能夠進行修改。此時,會建立一個新的Pair對象(casPair方法,由於Pair是Immutable類)。

但這裏有段優化邏輯,就是若是 newReference == current.reference && newStamp == current.stamp,說明用戶修改的新值和AtomicStampedReference中目前持有的值徹底一致,那麼其實不須要修改,直接返回true便可。

AtomicStampedReference接口聲明

clipboard.png

4、AtomicMarkableReference

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

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

可是,有時候,咱們並不關心引用變量更改了幾回,只是單純的關心是否更改過,因此就有了AtomicMarkableReference

clipboard.png

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

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

AtomicMarkableReference接口聲明

clipboard.png

相關文章
相關標籤/搜索