Java併發 -- Lock + Condition
- 併發領域的兩大核心問題:互斥 +同步
- 互斥:同一時刻只允許一個執行緒訪問共享資源
- 同步:執行緒之間的通訊和協作
- JUC通過Lock和Condition兩個介面實現管程 ,其中Lock 用於解決互斥 問題,而Condition 用於解決同步 問題
再造管程的理由
- Java語言對管程的原生實現:synchronized
- 在Java 1.5中,synchronized的效能 不如JUC中的Lock,在Java 1.6中,synchronized做了很多的效能優化
-
再造管程的核心理由
:synchronized無法破壞不可搶佔條件
(死鎖的條件之一)
- synchronized在申請資源的時候,如果申請不到,執行緒直接進入阻塞狀態 ,也不會釋放執行緒已經佔有的資源
- 更合理的情況:佔用部分資源的執行緒如果進一步申請其它資源的時,如果申請不到,可以主動釋放 它所佔有的資源
-
解決方案
-
能夠響應中斷
- synchronized:持有鎖A的執行緒在嘗試獲取鎖B失敗,進入阻塞 狀態,如果發生死鎖 ,將沒有機會喚醒 阻塞執行緒
- 如果處於阻塞狀態的執行緒能夠響應中斷訊號,那阻塞執行緒就有機會釋放曾經持有的鎖A
-
支援超時
- 如果執行緒在一段時間內沒有獲得鎖,不是進入阻塞狀態,而是返回一個錯誤
- 那麼該執行緒也有機會釋放曾經持有的鎖
-
非阻塞地獲取鎖
- 如果嘗試獲取鎖失敗,不是進入阻塞狀態,而是直接返回 ,那麼該執行緒也有機會釋放曾經持有的鎖
-
能夠響應中斷
// java.util.concurrent.locks.Lock介面 // 能夠響應中斷 void lockInterruptibly() throws InterruptedException; // 支援超時(同時也能夠響應中斷) boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 非阻塞地獲取鎖 boolean tryLock();
保證可見性
public class Counter { private final Lock lock = new ReentrantLock(); private int value; public void addOne() { // 獲取鎖 lock.lock(); try { // 可見性:執行緒T1執行value++,後續的執行緒T2能看到正確的結果 value++; } finally { // 釋放鎖 lock.unlock(); } } }
// ReentrantLock的虛擬碼 public class SimpleLock { // 利用了volatile相關的Happens-Before規則 private volatile int state; // 加鎖 public void lock() { // 讀取state state = 1; } // 解鎖 public void unlock() { // 讀取state state = 0; } }
-
Java多執行緒的可見性
是通過Happens-Before
規則來保證的
- synchronized的可見性保證:synchronized的解鎖Happens-Before於後續對這個鎖的加鎖
- JUC中Lock的可見性保證: 利用了volatile相關的Happens-Before規則
-
ReentrantLock內部持有一個volatile
的成員變數state,加鎖和解鎖時都會讀寫state
- 執行value++之前 ,執行lock ,會讀寫 volatile變數state
- 執行value++之後 ,執行unlock ,會讀寫 volatile變數state
-
相關的Happens-Before規則
-
順序性規則
-
對於執行緒T1,
value++
Happens-Beforeunlock()
-
對於執行緒T2,
lock()
Happens-Before讀取value
-
對於執行緒T1,
-
volatile變數規則
-
對於執行緒T1,unlock()會執行
state=1
- 對於執行緒T2,lock()會先讀取state
- volatile變數的寫操作 Happens-Before volatile變數的讀操作
- 因此執行緒T1的unlock Happens-Before執行緒T2的lock ,與synchronized非常類似
-
對於執行緒T1,unlock()會執行
- 傳遞性規則:執行緒T1的value++ Happens-Before 執行緒T2的lock()
-
順序性規則
可重入鎖
public class X { private final Lock lock = new ReentrantLock(); private int value; private int get() { lock.lock(); // 2 try { return value; } finally { lock.unlock(); } } public void addOne() { lock.lock(); try { value = get() + 1; // 1 } finally { lock.unlock(); } } }
- 可重入鎖:執行緒可以 重複獲取同一把鎖
- 執行路徑:addOne -> get,在執行到2時,如果鎖是可重入的,那麼執行緒會再次加鎖成功,否則會被阻塞
公平鎖和非公平鎖
// java.util.concurrent.locks.ReentrantLock public ReentrantLock() { // 預設非公平鎖 sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
- 在管程模型中,每把鎖都對應著一個 入口等待佇列
- 如果一個執行緒沒有獲得鎖,就會進入入口等待佇列,當有執行緒釋放鎖的時候,需要從入口等待佇列中喚醒一個等待的執行緒
- 喚醒策略:如果是公平鎖 ,喚醒等待時間最長 的執行緒,如果是非公平鎖,隨機喚醒
鎖的最佳實踐
- 永遠只在更新物件的成員變數 時加鎖
- 永遠只在訪問可變的成員變數 時加鎖
-
永遠不在呼叫其它物件的方法
時加鎖,因為呼叫其它物件的方法是不安全
的(對其它物件的方法不瞭解)
- 可能有Thread.sleep(),也有可能有慢IO,這會嚴重影響效能
- 甚至還會加鎖,這有可能導致死鎖
- 減少鎖的持有時間
- 減少鎖粒度
轉載請註明出處:http://zhongmingmao.me/2019/05/05/java-concurrent-lock-condition/
訪問原文「Java併發 -- Lock + Condition 」獲取最佳閱讀體驗並參與討論