Java多線程 開發中避免死鎖的八種方法


1. 設置超時時間

使用JUC包中的Lock接口提供的tryLock方法. 
該方法在獲取鎖的時候, 可以設置超時時間, 如果超過了這個時間還沒拿到這把鎖, 那么就可以做其他的事情, 而不是像 synchronized 如果沒有拿到鎖會一直等待下去.

                      boolean  tryLock  (  long time  , TimeUnit unit  )  throws InterruptedException  ;  

造成超時的原因有很多種:發生了死鎖, 線程進入了死循環, 線程邏輯復雜執行慢.

到了超時時間, 那么就獲取鎖失敗, 就可以做一些記錄操作, 例如 打印錯誤日志, 發送報警郵件,提示運維人員重啟服務等等.

如下的代碼演示了 使用tryLock 來避免死鎖的案例. 
線程1 如果拿到了鎖1 , 那么就在指定的800毫秒內去嘗試拿到鎖2, 如果兩把鎖都拿到了 , 那么就釋放這兩把鎖. 如果在指定的時間內, 沒有拿到鎖2 , 那么就釋放鎖1 .

線程2 與線程1相反, 先去嘗試拿到鎖2, 如果拿到了, 就去在3s內嘗試拿到鎖1, 如果拿到了, 那么就釋放鎖1和2, 如果3s內沒有拿到鎖1, 那么釋放鎖2 .

                      package com  . thread  . deadlock  ;  import java  . util  . Random  ;  import java  . util  . concurrent  . TimeUnit  ;  import java  . util  . concurrent  . locks  . Lock  ;  import java  . util  . concurrent  . locks  . ReentrantLock  ;  /** * 類名稱:TryLockDeadlock * 類描述: 使用lock接口提供的trylock 避免死鎖 * * @author: https://javaweixin6.blog.csdn.net/ * 創建時間:2020/9/12 17:23 * Version 1.0 */  public  class  TryLockDeadlock  implements  Runnable  {  int flag  =  1  ;  //ReentrantLock 為可重入鎖  static Lock lock1  =  new  ReentrantLock  (  )  ;  static Lock lock2  =  new  ReentrantLock  (  )  ;  public  static  void  main  ( String  [  ] args  )  {  // 創建兩個線程 給出不同的flag 並啟動 TryLockDeadlock r1  =  new  TryLockDeadlock  (  )  ; TryLockDeadlock r2  =  new  TryLockDeadlock  (  )  ; r1  . flag  =  1  ; r2  . flag  =  0  ;  new  Thread  ( r1  )  .  start  (  )  ;  new  Thread  ( r2  )  .  start  (  )  ;  }  @Override  public  void  run  (  )  {  for  (  int i  =  0  ; i  <  100  ; i  ++  )  {  if  ( flag  ==  1  )  {  //先獲取鎖1 再獲取鎖2  try  {  //給鎖1 800毫秒與獲取鎖, 如果拿到鎖, 返回true, 反之返回false  if  ( lock1  .  tryLock  (  800  , TimeUnit  . MICROSECONDS  )  )  { System  . out  .  println  (  "線程1獲取到了鎖1 "  )  ;  //隨機的休眠 Thread  .  sleep  (  new  Random  (  )  .  nextInt  (  1000  )  )  ;  if  ( lock2  .  tryLock  (  800  , TimeUnit  . MICROSECONDS  )  )  { System  . out  .  println  (  "線程1獲取到了鎖2 "  )  ; System  . out  .  println  (  " 線程1 成功獲取了兩把鎖 "  )  ;  //釋放兩把鎖, 退出循環 lock2  .  unlock  (  )  ; lock1  .  unlock  (  )  ;  break  ;  }  else  { System  . out  .  println  (  " 線程1嘗試獲取鎖2 失敗, 已經重試 "  )  ;  //釋放鎖1 lock1  .  unlock  (  )  ;  //隨機的休眠 Thread  .  sleep  (  new  Random  (  )  .  nextInt  (  1000  )  )  ;  }  }  else  { System  . out  .  println  (  " 線程1 獲取鎖1失敗, 已重試 "  )  ;  }  }  catch  (  InterruptedException e  )  { e  .  printStackTrace  (  )  ;  }  }  if  ( flag  ==  0  )  {  //先獲取鎖2 再獲取鎖1. 並且嘗試獲取鎖的時間變長 ,改成3s  try  {  //給鎖1 800毫秒與獲取鎖, 如果拿到鎖, 返回true, 反之返回false  if  ( lock2  .  tryLock  (  3000  , TimeUnit  . MICROSECONDS  )  )  { System  . out  .  println  (  "線程2獲取到了鎖2 "  )  ;  //隨機的休眠 Thread  .  sleep  (  new  Random  (  )  .  nextInt  (  1000  )  )  ;  if  ( lock1  .  tryLock  (  3000  , TimeUnit  . MICROSECONDS  )  )  { System  . out  .  println  (  "線程2獲取到了鎖1 "  )  ; System  . out  .  println  (  " 線程2 成功獲取了兩把鎖 "  )  ;  //釋放兩把鎖, 退出循環 lock1  .  unlock  (  )  ; lock2  .  unlock  (  )  ;  break  ;  }  else  { System  . out  .  println  (  " 線程2嘗試獲取鎖1 失敗, 已經重試 "  )  ;  //釋放鎖2 lock2  .  unlock  (  )  ;  //隨機的休眠 Thread  .  sleep  (  new  Random  (  )  .  nextInt  (  1000  )  )  ;  }  }  else  { System  . out  .  println  (  " 線程2 獲取鎖2失敗, 已重試 "  )  ;  }  }  catch  (  InterruptedException e  )  { e  .  printStackTrace  (  )  ;  }  }  }  }  }  

運行程序后, 此時打印的情況如下: 
線程1和2 ,分別拿到了鎖1 和2 . 如果此時是用 synchronized 加鎖的, 那么就會進入死循環的情況 , 因為 此時線程1是要去獲取鎖2的, 而此時鎖2被線程2持有着 , 線程2此時要獲取鎖1 ,而鎖1被線程2持有, 那么就會造成死鎖. 
而使用trylock后, 如下圖打印, 線程1在嘗試800ms獲取鎖2失敗后, 釋放了鎖1, 那么此時鎖2就獲得了鎖1, 線程2獲得了兩把鎖, 釋放了這兩把鎖, 接着線程1就獲得了這兩把鎖. 
 
再次運行程序, 此時程序打印如下 . 可以看到線程2兩次獲取鎖1 失敗 , 兩次獲得了CPU的執行權, 可能是由於線程1休眠時間過長導致的. 
線程2重復2次失敗獲取鎖1失敗后, 線程1蘇醒, 獲得了2把鎖, 並且釋放了兩把鎖, 線程2之后也獲得了2把鎖. 

2. 多使用JUC包提供的並發類,而不是自己設計鎖

JDK1.5后, 有JUC包提供並發類, 而不需要自己用wait 和notify來進行線程間的通信操作 , 這些成熟的並發類已經考慮的場景很完備了, 比自己設計鎖更加安全. 
JUC中的並發類 例如 ConcurrentHashMap ConcurrentLinkedQueue AtomicBoolean 等等 
實際應用中 java.util.concurrent.atomic 包中提供的類使用廣泛, 簡單方便, 並且效率比Lock更高.

多用並發集合, 而不是用同步集合. 
例如用 ConcurrentHashMap , 而不是使用下圖中 Collections 工具類提供的同步集合. 因為同步集合性能低 

3. 盡量降低鎖的使用粒度

盡量降低鎖的使用粒度 : 用不同的鎖 ,而不是同一個鎖. 
整個類如果使用一個鎖來保護的話, 那么效率會很低, 而且有死鎖的風險, 很多線程都來用這把鎖的話, 就容易造成死鎖. 
鎖的使用范圍, 只要能滿足業務要求, 范圍越小越好.雅思5.5是什么水平

4. 盡量使用同步方法 而不是同步代碼塊

如果能使用同步代碼塊, 就不要使用同步方法, 
好處有兩點 :

  1. 同步方法是把整個方法給加上鎖給同步了, 范圍較大,造成性能低下, 使用同步代碼塊范圍小,性能高.
  2. 使用同步代碼塊, 可以自己指定鎖的對象, 這樣有了鎖的控制權, 這樣也能避免發生死鎖

5. 給線程起有意義的名字

給線程起有意義的名字, 是便於在測試環境和生產環境排查bug和事故的時候快速定位問題. 
一些開源的框架和JDK都遵循了給線程起名字的規范

6. 避免鎖的嵌套

如下的文章<必然發生死鎖>例子中的代碼就是鎖的嵌套. 拿一個鎖, 接着再拿一個鎖. 並且使用的還是sleep這種不會釋放鎖的方式, 即拿到一個鎖之后,不會去釋放鎖. 
那么如果獲取鎖的順序相反了, 就會造成死鎖的發生! 
https://javaweixin6.blog.csdn.net/article/details/108460550 

7. 分配鎖資源之前先看能不能收回來資源

分配鎖資源之前先看能不能收回來資源: 即在分配給某個線程鎖資源之前, 先計算一下如果分配出去了, 會不會造成死鎖的情況, 也就是能不能回收得回來, 如果不能回收回來, 那么就會造成死鎖, 那就不分配鎖資源給這個線程 , 如果能回收回來, 那么就分配資源下去.

此種思想的實現有 銀行家算法 來避免死鎖的發生. 可以參考如下的文章 
https://blog.csdn.net/u014634576/article/details/52600826

https://mp.weixin.qq.com/s?__biz=MzAwNzczMjk1NQ==&mid=400637315&idx=1&sn=f578bf6de58c1a57df07df310ae1ca1b&scene=1&srcid=0920DQXmm3IeDGyaJxxLz6oZ#wechat_redirect

https://www.cnblogs.com/128-cdy/p/12188340.html

8. 專鎖專用

盡量不要幾個功能用同一把鎖. 來避免鎖的沖突, 如果都用同一把鎖, 那么就容易造成死鎖.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM