一. synchronized
在 JDK 1.6 之前,synchronized 是重量級鎖,效率低下。
從 JDK 1.6 開始,synchronized 做了很多優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。
synchronized 同步鎖一共包含四種狀態:無鎖、偏向鎖、輕量級鎖、重量級鎖,它會隨著競爭情況逐漸升級。synchronized 同步鎖可以升級但是不可以降級,目的是為了提高獲取鎖和釋放鎖的效率。
synchronized 的底層原理
synchronized 修飾的程式碼塊
通過反編譯.class檔案,通過檢視位元組碼可以得到:在程式碼塊中使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令指明同步程式碼塊的結束位置。
synchronized 修飾的方法
同樣檢視位元組碼可以得到:在同步方法中會包含 ACC_SYNCHRONIZED 標記符。該標記符指明瞭該方法是一個同步方法,從而執行相應的同步呼叫。
二. 物件鎖、類鎖、私有鎖
物件鎖:使用 synchronized 修飾非靜態的方法以及 synchronized(this) 同步程式碼塊使用的鎖是物件鎖。
類鎖:使用 synchronized 修飾靜態的方法以及 synchronized(class) 同步程式碼塊使用的鎖是類鎖。
私有鎖:在類內部宣告一個私有屬性如private Object lock,在需要加鎖的同步塊使用 synchronized(lock)
它們的特性:
- 物件鎖具有可重入性。
- 當一個執行緒獲得了某個物件的物件鎖,則該執行緒仍然可以呼叫其他任何需要該物件鎖的 synchronized 方法或 synchronized(this) 同步程式碼塊。
- 當一個執行緒訪問某個物件的一個 synchronized(this) 同步程式碼塊時,其他執行緒對該物件中所有其它 synchronized(this) 同步程式碼塊的訪問將被阻塞,因為訪問的是同一個物件鎖。
- 每個類只有一個類鎖,但是類可以例項化成物件,因此每一個物件對應一個物件鎖。
- 類鎖和物件鎖不會產生競爭。
- 私有鎖和物件鎖也不會產生競爭。
- 使用私有鎖可以減小鎖的細粒度,減少由鎖產生的開銷。
由私有鎖實現的等待/通知機制:
Object lock = new Object();
// 由等待方執行緒實現
synchronized (lock) {
while (條件不滿足) {
lock.wait();
}
}
// 由通知方執行緒實現
synchronized (lock) {
條件發生改變
lock.notify();
}
複製程式碼
三. ReentrantLock
ReentrantLock 是一個獨佔/排他鎖。相對於 synchronized,它更加靈活。但是需要自己寫出加鎖和解鎖的過程。它的靈活性在於它擁有很多特性。
ReentrantLock 需要顯示地進行釋放鎖。特別是在程式異常時,synchronized 會自動釋放鎖,而 ReentrantLock 並不會自動釋放鎖,所以必須在 finally 中進行釋放鎖。
它的特性:
- 公平性:支援公平鎖和非公平鎖。預設使用了非公平鎖。
- 可重入
- 可中斷:相對於 synchronized,它是可中斷的鎖,能夠對中斷作出響應。
- 超時機制:超時後不能獲得鎖,因此不會造成死鎖。
ReentrantLock 是很多類的基礎,例如 ConcurrentHashMap 內部使用的 Segment 就是繼承 ReentrantLock,CopyOnWriteArrayList 也使用了 ReentrantLock。
四. ReentrantReadWriteLock
我之前寫過一篇文章《ReentrantReadWriteLock讀寫鎖及其在 RxCache 中的使用》 曾詳細介紹過ReentrantReadWriteLock。
它擁有讀鎖(ReadLock)和寫鎖(WriteLock),讀鎖是一個共享鎖,寫鎖是一個排他鎖。
它的特性:
- 公平性:支援公平鎖和非公平鎖。預設使用了非公平鎖。
- 可重入:讀執行緒在獲取讀鎖之後能夠再次獲取讀鎖。寫執行緒在獲取寫鎖之後能夠再次獲取寫鎖,同時也可以獲取讀鎖(鎖降級)。
- 鎖降級:先獲取寫鎖,再獲取讀鎖,然後再釋放寫鎖的過程。鎖降級是為了保證資料的可見性。
五. CAS
上面提到的 ReentrantLock、ReentrantReadWriteLock 都是基於 AbstractQueuedSynchronizer (AQS),而 AQS 又是基於 CAS。CAS 的全稱是 Compare And Swap(比較與交換),它是一種無鎖演算法。
synchronized、Lock 都採用了悲觀鎖的機制,而 CAS 是一種樂觀鎖的實現。
CAS 的特性:
- 通過呼叫 JNI 的程式碼實現
- 非阻塞演算法
- 非獨佔鎖
CAS 存在的問題:
- ABA
- 迴圈時間長開銷大
- 只能保證一個共享變數的原子操作
六. Condition
Condition 用於替代傳統的 Object 的 wait()、notify() 實現執行緒間的協作。
在 Condition 物件中,與 wait、notify、notifyAll 方法對應的分別是 await、signal 和 signalAll。
Condition 必須要配合 Lock 一起使用,一個 Condition 的例項必須與一個 Lock 繫結。
它的特性:
- 一個 Lock 物件可以建立多個 Condition 例項,所以可以支援多個等待佇列。
- Condition 在使用 await、signal 或 signalAll 方法時,必須先獲得 Lock 的 lock()
- 支援響應中斷
- 支援的定時喚醒功能
七. Semaphore
Semaphore、CountDownLatch、CyclicBarrier 都是併發工具類。
Semaphore 可以指定多個執行緒同時訪問某個資源,而 synchronized 和 ReentrantLock 都是一次只允許一個執行緒訪問某個資源。由於 Semaphore 適用於限制訪問某些資源的執行緒數目,因此可以使用它來做限流。
Semaphore 並不會實現資料的同步,資料的同步還是需要使用 synchronized、Lock 等實現。
它的特性:
- 基於 AQS 的共享模式
- 公平性:支援公平模式和非公平模式。預設使用了非公平模式。
八. CountDownLatch
CountDownLatch 可以看成是一個倒計數器,它允許一個或多個執行緒等待其他執行緒完成操作。因此,CountDownLatch 是共享鎖。
CountDownLatch 的 countDown() 方法將計數器減1,await() 方法會阻塞當前執行緒直到計數器變為0。
在我的爬蟲框架NetDiscovery中就使用了 CountDownLatch 來控制某個爬蟲的暫停和恢復。
九. 鎖的分類
十. 總結
本文小結了 Java 常用的一些鎖及其一些特性,掌握這些鎖是掌握 Java 併發程式設計的基礎。當然,Java 的鎖並不止這些,例如 ConcurrentHashMap 的分段鎖(Segment),分散式環境下所使用的分散式鎖。
參考資料:
- 《Java併發程式設計藝術》
- 《Java併發程式設計實戰》
- 不可不說的Java“鎖”事
Java與Android技術棧:每週更新推送原創技術文章,歡迎掃描下方的公眾號二維碼並關注,期待與您的共同成長和進步。