close
前一段時間在刷 LeetCode 多線程相關題目的時候,看到使用 ReentrantLock 有兩種不同的寫法。

方式一:Oracle 官方推薦的寫法

privatevallook=ReentrantLock()funprintNumber(){look.lock()try{//TODO}finally{look.unlock()}}

方式二:錯誤的寫法

privatevallook=ReentrantLock()funprintNumber(){try{look.lock()}finally{look.unlock()}}

方法一 是 Oracle 推薦的方式, 並且在 阿里巴巴JAVA開發手冊 明確規定了不建議使用 方式二, 即不建議將 lock.lock() 寫在 try...finally 代碼塊內部,一起來分析都有哪些需要注意的細節。

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantLock.htmlhttps://github.com/alibaba/p3c/issues/287

通過這篇文章,將會學習到以下內容:

lock() 方法為什麼不能放在 try...finally 代碼塊內部?
lock() 方法放在 try...finally 代碼塊內部第一行安全嗎?
為什麼要在 finally 代碼塊中執行 unlock() 方法?
lock() 方法放在 try 代碼塊外部一定安全嗎?
lock() 方法為什麼不能放在 try...finally 代碼塊內部

避免由於其他代碼段拋出異常,造成加鎖失敗,導致在 finally 代碼塊中調用 unlock() 解鎖方法,對未加鎖的對象進行解鎖,從而拋出 IllegalMonitorStateException 異常(依賴具體的實現), unlock() 源碼很簡單,如下所示。java/util/concurrent/locks/ReentrantLock.java

publicvoidunlock(){sync.release(1);}

unlock 方法被委派到了 Sync 類上,Sync 繼承自 AbstractQueuedSynchronizer。java/util/concurrent/locks/AbstractQueuedSynchronizer.java

publicfinalbooleanrelease(intarg){if(tryRelease(arg)){//......returntrue;}returnfalse;}//供子類重寫protectedbooleantryRelease(intarg){thrownewUnsupportedOperationException();}

子類 ReentrantLock 和 ReentrantReadWriteLock 都會重寫 tryRelease 方法。這裡主要看一下 ReentrantLock#tryRelease 方法。

protectedfinalbooleantryRelease(intreleases){if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();//......returnfree;}

在 tryRelease 方法中,會判斷當前線程是否等於擁有鎖的線程,如果不相等則表示加鎖失敗,會拋出 IllegalMonitorStateException 異常。同時也會造成真正的異常信息被覆蓋掉,代碼如下所示。

funprintNumber(){try{valnumber=1/0look.lock()//TODO}finally{look.unlock()}}

正如代碼所示,希望出現的異常信息應該是 java.lang.ArithmeticException: / by zero, 但是實際運行的時候,異常信息如下所示,真正的異常信息被覆蓋掉了。

為什麼要在 finally 代碼塊中執行 unlock() 方法

既然 unlock() 方法會拋出異常,為什麼還要將它放在 finally 代碼塊中,這是為了保證執行過程中出現異常,依然能夠保證鎖會被釋放掉,避免死鎖。

注意:需要將 unlock() 方法放到 finally 代碼塊第一行。

lock() 方法寫在 try...finally 代碼塊內部第一行安全嗎

我在網上看到部分回答,說可以將 lock() 方法寫在 try...finally 代碼塊內部第一行,即 lock() 方法前不會添加其他代碼,但是這樣真的安全嗎?不一定,只能說出現問題的概率很低,一起來看一下源碼描述。

根據 lock() 方法的描述,可能拋出 unchecked 異常(依賴具體的實現), 如果放在 try...finally 代碼塊內部,必然會觸發 finally 代碼塊中 unlock() 方法。在 unlock() 方法中會檢查是否持有鎖,未持有鎖則會拋出 IllegalMonitorStateException 異常(依賴具體的實現)。

根據 unlock() 方法的描述,通常只有鎖的持有者才能釋放鎖,也就是說當非鎖持有線程調用 unlock() 方法時會拋出 unchecked 異常,雖然兩個方法都是因為加鎖失敗導致的,但是真正的異常信息會被 unlock() 方法拋出的異常信息覆蓋掉。

lock() 方法寫在 try 代碼塊外部一定安全嗎

將 lock() 方法放在 try 代碼塊外部一定安全嗎?不一定,取決於我們的代碼是如何實現的,異常代碼如下所示。

funprintNumber(){look.lock()//拋出異常的代碼try{//......}finally{//最後保證鎖會被釋放掉look.unlock()}}

在加鎖方法 lock() 和 try 代碼塊之間拋出了異常,那麼就會出現加鎖成功,但是無法解鎖,會造成其他線程無法獲取鎖。

如何避免以上的問題的發生

在使用 ReentrantLock 獲取鎖的時候,需要注意以下幾點:

look() 方法必須寫在 try 代碼塊之外
look() 方法和 try...finally 代碼塊之間,沒有其他的代碼段,避免出現無法解鎖,造成其他線程無法獲取到鎖
unlock() 要放到 finally 代碼塊第一行

調用 lock() 方法,採用阻塞方式獲取鎖,如果失敗了,則會進入阻塞等待狀態

privatevallook=ReentrantLock()funprintNumber(){look.lock()try{//TODO}finally{look.unlock()}}

調用 tryLock() 方法,嘗試去獲取鎖,如果失敗了,則會立即返回 false

privatevallook=ReentrantLock()funprintNumber(){valisLocked=look.tryLock()if(isLocked){try{//TODO}finally{look.unlock()}}}

技術交流,歡迎加我微信:ezglumes ,拉你入技術交流群。

私信領取相關資料

推薦閱讀:

音視頻開發工作經驗分享 || 視頻版

OpenGL ES 學習資源分享

開通專輯 | 細數那些年寫過的技術文章專輯

Android NDK 免費視頻在線學習!!!

你想要的音視頻開發資料庫來了

推薦幾個堪稱教科書級別的 Android 音視頻入門項目

覺得不錯,點個在看唄~

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()