Java並發——原子變量和原子操作


      很多情況下我們只是需要一個簡單的、高效的、線程安全的遞增遞減方案。注意,這里有三個條件:簡單,意味着程序員盡可能少的操作底層或者實現起來要比較容易;高效意味着耗用資源要少,程序處理速度要快;線程安全也非常重要,這個在多線程下能保證數據的正確性。這三個條件看起來比較簡單,但是實現起來卻難以令人滿意。

      通常情況下,在Java里面,++i或者--i不是線程安全的,這里面有三個獨立的操作:獲得變量當前值,為該值+1/-1,然后寫回新的值。在沒有額外資源可以利用的情況下,只能使用加鎖才能保證讀-改-寫這三個操作是“原子性”的。

      Java 5新增了AtomicInteger類,該類包含方法getAndIncrement()以及getAndDecrement(),這兩個方法實現了原子加以及原子減操作,但是比較不同的是這兩個操作沒有使用任何加鎖機制,屬於無鎖操作。      

      在JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這會導致有鎖(后面的章節還會談到鎖)。

      鎖機制存在以下問題:

      (1)在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。

      (2)一個線程持有鎖會導致其它所有需要此鎖的線程掛起。

      (3)如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。

      volatile是不錯的機制,但是volatile不能保證原子性。因此對於同步最終還是要回到鎖機制上來。

      獨占鎖是一種悲觀鎖,synchronized就是一種獨占鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。

CAS 操作

      上面的樂觀鎖用到的機制就是CAS,Compare and Swap。

      CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。

非阻塞算法 (nonblocking algorithms)

一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的算法。

      現代的CPU提供了特殊的指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。

      拿出AtomicInteger來研究在沒有鎖的情況下是如何做到數據正確性的。

private volatile int value;

    首先毫無疑問,在沒有鎖的機制下需要借助volatile原語,保證線程間的數據是可見的(共享的),這樣獲取變量值的時候才能直接讀取。

public final int get() {
        return value;
    }

      然后來看看++i是怎么做到的。

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

      在這里采用了CAS操作,每次從內存中讀取數據然后將此數據和+1后的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止。

      而compareAndSet利用JNI來完成CPU指令的操作。

public final boolean compareAndSet(int expect, int update) {   
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

      整體的過程就是這樣子的,利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。

      而整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞算法,J.U.C在性能上有了很大的提升。參考資料的文章中介紹了如果利用CAS構建非阻塞計數器、隊列等數據結構。

      CAS看起來很爽,但是會導致“ABA問題”。

      CAS算法實現一個重要前提需要取出內存中某時刻的數據,而在下時刻比較並替換,但是在這個時間差內任何變化都可能發生。

      比如說一個線程one從內存位置V中取出A,這時候另一個線程two也從內存中取出A,並且two進行了一些操作變成了B,然后two又將V位置的數據變成A,這時候線程one進行CAS操作發現內存中仍然是A,然后one操作成功。盡管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。如果鏈表的頭在變化了兩次后恢復了原值,但是不代表鏈表就沒有變化。要解決"ABA問題",我們需要增加一個版本號,在更新變量值的時候不應該只更新一個變量值,而應該更新兩個值,分別是變量值和版本號,AtomicStampedReference支持在兩個變量上進行原子的條件更新,可以使用該類進行更新操作。

參考資料:

(1)非阻塞算法簡介

(2)流行的原子

 

轉自:http://www.blogjava.net/xylz/archive/2010/07/04/325206.html

 


免責聲明!

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



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