在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)流行的原子
©2009-2014 IMXYLZ
|求賢若渴