在JDK1.5之前。Java主要靠synchronized這個關鍵字保證同步,已解決多線程下的線程不安全問題,但是這會導致鎖的發生,會引發一些個性能問題。
鎖主要存在一下問題
(1)在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
(2)一個線程持有鎖會導致其它所有需要此鎖的線程掛起。
(3)如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。
Volatile是一個不錯的選擇,但是前面我們已經說了,volatile不能保證原子性,因此同步還是需要用到鎖。
也許大家已經聽說過,鎖分兩種,一個叫悲觀鎖,一種稱之為樂觀鎖。Synchronized就是悲觀鎖的一種,也稱之為獨占鎖,加了synchronized關鍵字的代碼基本上就只能以單線程的形式去執行了,它會導致其他需要該資源的線程掛起,直到前面的線程執行完畢釋放所資源。而另外一種樂觀鎖是一種更高效的機制,它的原理就是每次不加鎖去執行某項操作,如果發生沖突則失敗並重試,直到成功為止,其實本質上不算鎖,所以很多地方也稱之為自旋。
樂觀鎖用到的主要機制就是CAS。Compare And Swap。
CAS有三個操作數,內存數據v,舊的預期數據A,要修改的數據B。每次進行數據更新時,當且僅當預期值A和內存中的數據V相同時,才將內存中的數據修改為B,否則什么也不做。
使用這種機制編寫的算法也叫非阻塞算法,標准定義為一個線程的失敗或者掛起不影響其他線程的失敗或者掛起的算法。
現在的CPU提供了特殊的指令來自動更新共享數據,而且能檢測到其他數據的干擾,因此可以通過compareAndSet來提到鎖定。前面我們提到的一些原子類其實就是用的這個原理,如AtomicInteger,我們來看一下它對應的源碼。
private volatile int value; public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } } public final int getAndAdd(int delta) { for (;;) { int current = get(); int next = current + delta; if (compareAndSet(current, next)) return current; } }
這里很顯然采取了CAS的機制,每次從內存中讀取數據都需要和+1后的數據進行一次CAS操作,如果成功返回結果,否則就失敗重試,直到重試成功為止!
但是方法compareAndSet卻是利用JNI來完成CPU指令的操作。
我們來看一下對應的源碼
// setup to use Unsafe.compareAndSwapInt for updates private static final Unsafe unsafe = Unsafe.getUnsafe(); public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
整個過程就是這樣子的,利用CPU的CAS機制,同時借助JNI來完成Java的非阻塞算法。基本上Java中的原子類都是使用類似的機制來保證數據的原子操作的。
盡管CAS機制使得我們可以不依賴同步,不影響和掛起其他線程實現原子性操作,能大大提升運行時的性能,但是會導致一個ABA的問題。如線程一和線程二都取出了主存中的數據為A,這時候線程二將數據修改為B,然后又修改為A,此時線程一再次去進行compareAndSet的時候仍然能夠匹配成功,而實際對的數據已經發生了變更了,只不過發生了2次變化將對應的值修改為原始的數據了,並不代表實際數據沒有發生變化。這時候前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。這允許一對變化的元素進行原子操作。
后續我們將繼續探討一下鎖機制的實現,Java並發包中的鎖是如何保證操作正確性的同時又大幅提升性能的。