多執行緒之:Synchronized與ReentrantLock
什麼是執行緒安全
- 保證多執行緒環境下共享的、可修改的狀態的正確性。(這裡的狀態在程式中可以看作為資料)
- 反著來說則是如果狀態非共享、不可修改,也就不存線上程安全的問題
保證執行緒安全的兩種方法
- 封裝,通過封裝將物件內部狀態隱藏、保護起來
- 不可變,將狀態改為不可變,例如將狀態定義為
final
執行緒安全要保證的基本特性
- 原子性
相關操作不會在中途被其他執行緒所幹擾,一般通過同步機制實現 - 可見性
一個行程修改了某個共享變數,其新狀態能夠立即被其他執行緒知曉,通常被解釋為將執行緒本地狀態反映到主記憶體上,volatile就是負責保證可見性的 - 有序性
保證執行緒內序列語義,避免指令重排
Synchronized與ReentrantLock
synchronized
synchronized
可以很方便的解決多執行緒間資源共享同步的問題,也就是我們平常所說的執行緒安全問題。
它可以修飾方法和程式碼塊,無法是用作何種修飾,synchronized
獲取的鎖都是物件。
關於synchronized
的使用這裡就不說了。
ReentrantLock
ReentrantLock
一般稱為再入鎖,是Lock的實現類,是一個互斥的同步器。
再入鎖通過程式碼直接呼叫 lock()
方法獲取,程式碼書寫也更加靈活。同時ReentrantLock
提供了很多實用的方法,能夠實現很多synchronized
無法做到的細節控制,比如可以控制 fairness,也就是公平性,或者利用條件定義等。
但是,編碼中也需要注意,必須要明確呼叫 unlock() 方法釋放,不然就會一直持有該鎖。
條件變數(Condition)
ReentrantLock
配合條件變數(java.util.concurrent.locks.Condition
),可以將複雜而晦澀的同步操作轉變為直觀可控的物件行為。
條件變數最為典型的應用場景就是標準類庫中的 ArrayBlockingQueue
等,看下原始碼:
通過再入鎖獲取條件變數:
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
兩個條件變數是從同一再入鎖創建出來,然後使用在特定操作中,如下面的 take 方法,判斷和等待條件滿足:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
當佇列為空時,試圖take獲取元素的執行緒會等待其他元素入隊操作的發生,而不是直接返回,這是 BlockingQueue 的語義,使用條件 notEmpty 就可以優雅地實現這一邏輯。
那麼,怎麼保證入隊觸發後續 take 操作呢?請看 enqueue 實現:
private void enqueue(E e) {
final Object[] items = this.items;
items[putIndex] = e;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal(); // 通知等待的執行緒,非空條件已經滿足
}
通過 signal/await 的組合,完成了條件判斷和通知等待執行緒,非常順暢就完成了狀態流轉。注意,signal 和 await 成對呼叫非常重要,不然假設只有 await 動作,執行緒會一直等待直到被打斷(interrupt)
效能比較
synchronized
和 ReentrantLock
的效能不能一概而論,早期版本 synchronized
在很多場景下效能相差較大,在後續版本進行了較多改進。
在低競爭場景中synchronized
表現可能優於 ReentrantLock
而在多執行緒高競爭條件下,ReentrantLock
比synchronized
有更加優異的效能表現。
高競爭
如果大部分情況,每個執行緒都不需要真的獲取鎖,就是低競爭;反之,大部分都要獲取鎖才能正常工作,就是高競爭
用法比較
- Lock使用起來比較靈活,但是必須有釋放鎖的配合動作
- Lock必須手動獲取與釋放鎖,而synchronized不需要手動釋放和開啟鎖
- Lock只適用於程式碼塊鎖,而synchronized可用於修飾方法、程式碼塊等
特性比較
ReentrantLock
的優勢體現在:
- 具備嘗試非阻塞地獲取鎖的特性:當前執行緒嘗試獲取鎖,如果這一時刻鎖沒有被其他執行緒獲取到,則成功獲取並持有鎖
- 能被中斷地獲取鎖的特性:與synchronized不同,獲取到鎖的執行緒能夠響應中斷,當獲取到鎖的執行緒被中斷時,中斷異常將會被丟擲,同時鎖會被釋放
- 超時獲取鎖的特性:在指定的時間範圍內獲取鎖;如果截止時間到了仍然無法獲取鎖,則返回
- 可以控制執行緒的競爭公平性
注意事項
在使用ReentrantLock
類的時,一定要注意三點:
- 在finally中釋放鎖,目的是保證在獲取鎖之後,最終能夠被釋放
- 不要將獲取鎖的過程寫在try塊內,因為如果在獲取鎖時發生了異常,異常丟擲的同時,也會導致鎖無故被釋放。
ReentrantLock
提供了一個newCondition
的方法,以便使用者在同一鎖的情況下可以根據不同的情況執行等待或喚醒的動作。