1. 程式人生 > >java double check lock

java double check lock

對於多執行緒程式設計來說,同步問題是我們需要考慮的最多的問題,同步的鎖什麼時候加,加在哪裡都需要考慮,當然在不影響功能的情況下,同步越少越好,鎖加的越遲越優是我們都必須認同的。DCL(Double Check Lock)就是為了達到這個目的。 

DCL簡單來說就是check-lock-check-act,先檢查再鎖,鎖之後再檢查一次,最後才執行操作。這樣做的目的是儘可能的推遲鎖的時間。網上普遍舉的一個例子是延遲載入的例子。

Java程式碼
  1. public   class  LazySingleton {  
  2.     private   static   volatile  LazySingleton instance;  
  3.     public   static  LazySingleton getInstantce() {  
  4.         if  (instance ==  null ) {  
  5.             synchronized  (LazySingleton. class ) {  
  6.                 if  (instance ==  null ) {  
  7.                     instance = new  LazySingleton();  
  8.                 }  
  9.             }  
  10.         }  
  11.         return
      instance;  
  12.     }  


對上面的例子來說,我們當然也可以把鎖載入方法上,那樣的話每次獲取例項都需 要獲取鎖,但其實對這個instance來說,只有在第一次建立例項的時候才需要同步,所以為了減少同步,我們先check了一下,看看這個 instance是否為空,如果為空,表示是第一使用這個instance,那就鎖住它,new一個LazySingleton的例項,下次另一個執行緒來 getInstance的時候,看到這個instance不為空,就表示已經建立過一個例項了,那就可以直接得到這個例項,避免再次鎖。這是第一個 check的作用。 

第二個check是解決鎖競爭情況下的問題,假設現在兩個執行緒來請求getInstance,A、B執行緒同時發現instance為空,因為我們 在方法上沒有加鎖,然後A執行緒率先獲得鎖,進入同步程式碼塊,new了一個instance,之後釋放鎖,接著B執行緒獲得了這個鎖,發現instance已 經被建立了,就直接釋放鎖,退出同步程式碼塊。所以這就是check-lock-then check。 
網上有很多文章討論DCL的失效問題,我就不贅述了,Java5之後可以通過將欄位宣告為volatile來避免這個問題。 
我推薦一篇很好的文章《用happen-before規則重新審視DCL》,裡面講的非常好。 

上面這個是最簡單的例子,網上隨處可見,雙重檢查的使用可不只限於單例的初始化,下面我舉個實際使用中的例子。 
快取使用者資訊,我們用一個hashmap做使用者資訊的快取,key是userId。

Java程式碼
  1. public   class  UserCacheDBService {  
  2.     private   volatile  Map<Long, UserDO> map =  new  ConcurrentHashMap<Long, UserDO>();  
  3.     private  Object mutex =  new  Object();  
  4.     /**  
  5.      * 取使用者資料,先從快取中取,快取中沒有再從DB取  
  6.      * @param userId  
  7.      * @return  
  8.      */   
  9.     public  UserDO getUserDO(Long userId) {  
  10.         UserDO userDO = map.get(userId);  
  11.         if (userDO ==  null ) {                            ① check  
  12.             synchronized (mutex) {                       ② lock  
  13.                 if  (!map.containsKey(userId)) {        ③ check  
  14.                     userDO = getUserFromDB(userId);    ④ act  
  15.                     map.put(userId, userDO);  
  16.                 }  
  17.             }  
  18.         }  
  19.         if (userDO ==  null ) {                             ⑤  
  20.             userDO = map.get(userId);  
  21.         }  
  22.         return  userDO;  
  23.     }  
  24.     private  UserDO getUserFromDB(Long userId) {  
  25.         // TODO Auto-generated method stub   
  26.         return   null ;  
  27.     }  
  28. }  
Java程式碼  收藏程式碼
  1. public class UserCacheDBService {  
  2.     private volatile Map<Long, UserDO> map = new ConcurrentHashMap<Long, UserDO>();  
  3.     private Object mutex = new Object();  
  4.     /** 
  5.      * 取使用者資料,先從快取中取,快取中沒有再從DB取 
  6.      * @param userId 
  7.      * @return 
  8.      */  
  9.     public UserDO getUserDO(Long userId) {  
  10.         UserDO userDO = map.get(userId);  
  11.         if(userDO == null) {                            ① check  
  12.             synchronized(mutex) {                       ② lock  
  13.                 if (!map.containsKey(userId)) {        ③ check  
  14.                     userDO = getUserFromDB(userId);    ④ act  
  15.                     map.put(userId, userDO);  
  16.                 }  
  17.             }  
  18.         }  
  19.         if(userDO == null) {                             ⑤  
  20.             userDO = map.get(userId);  
  21.         }  
  22.         return userDO;  
  23.     }  
  24.     private UserDO getUserFromDB(Long userId) {  
  25.         // TODO Auto-generated method stub  
  26.         return null;  
  27.     }  
  28. }  



三種做法: 
1、 沒有鎖,即沒有②和③,當在程式碼①處判斷userDO為空之後,直接從DB取資料,這種情況下有可能會造成資料的錯誤。舉個例子,A和B兩個執行緒,A執行緒 需要取使用者資訊,B執行緒更新這個user,同時把更新後的資料放入map。在沒有任何鎖的情況下,A執行緒在時間上先於B執行緒,A首先從DB取出這個 user,隨後執行緒排程,B執行緒更新了user,並把新的user放入map,最後A再把自己之前得到的老的user放入map,就覆蓋了B的操作。B以 為自己已經更新了快取,其實並沒有。 

2、 沒有第二次check,即沒有③的情況,在加鎖之後立即從DB取資料,這種情況可能會多幾次DB操作。同樣A和B兩個執行緒,都需要取使用者資訊,A和B在進 入程式碼①處時都發現map中沒有自己需要的user,隨後A執行緒率先獲得鎖,把新user放入map,釋放鎖,緊接著B執行緒獲得鎖,又從DB取了一次資料 放入map。 

3、 雙重檢查,取使用者資料的時候,我們首先從map中根據userId獲取UserDO,然後check是否取到了user(即user是否為空),如果沒有 取到,那就開始lock,然後再check一次map中是否有這個user資訊(避免其他執行緒先獲得鎖,已經往map中放了這個user),沒有的話,從 DB中得到user,放入map。 

4、 在⑤處又判斷了一次userDO為空的話就從map中取一次,這是由於此執行緒有可能在程式碼③處發現map中已經存在這個userDO,就沒有進行④操作。 

所以DCL只要記住:check-lock-check-act!