轉載自:http://www.blogjava.net/xylz/archive/2010/07/04/325206.html
在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操作成功,但是不代表這個過程就是沒有問題的。如果鏈表的頭在變化了兩次後恢複了原值,但是不代錶鏈表就沒有變化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。這允許一對變化的元素進行原子操作。
參考資料:
(1)非阻塞演算法簡介
(2)流行的原子