java double check lock
對於多執行緒程式設計來說,同步問題是我們需要考慮的最多的問題,同步的鎖什麼時候加,加在哪裡都需要考慮,當然在不影響功能的情況下,同步越少越好,鎖加的越遲越優是我們都必須認同的。DCL(Double Check Lock)就是為了達到這個目的。
DCL簡單來說就是check-lock-check-act,先檢查再鎖,鎖之後再檢查一次,最後才執行操作。這樣做的目的是儘可能的推遲鎖的時間。網上普遍舉的一個例子是延遲載入的例子。
- public class LazySingleton {
-
private static volatile LazySingleton instance;
- public static LazySingleton getInstantce() {
- if (instance == null ) {
- synchronized (LazySingleton. class ) {
- if (instance == null ) {
- instance = new LazySingleton();
- }
- }
- }
-
return
- }
- }
對上面的例子來說,我們當然也可以把鎖載入方法上,那樣的話每次獲取例項都需 要獲取鎖,但其實對這個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。
- public class UserCacheDBService {
- private volatile Map<Long, UserDO> map = new ConcurrentHashMap<Long, UserDO>();
- private Object mutex = new Object();
- /**
- * 取使用者資料,先從快取中取,快取中沒有再從DB取
- * @param userId
- * @return
- */
- public UserDO getUserDO(Long userId) {
- UserDO userDO = map.get(userId);
- if (userDO == null ) { ① check
- synchronized (mutex) { ② lock
- if (!map.containsKey(userId)) { ③ check
- userDO = getUserFromDB(userId); ④ act
- map.put(userId, userDO);
- }
- }
- }
- if (userDO == null ) { ⑤
- userDO = map.get(userId);
- }
- return userDO;
- }
- private UserDO getUserFromDB(Long userId) {
- // TODO Auto-generated method stub
- return null ;
- }
- }
- public class UserCacheDBService {
- private volatile Map<Long, UserDO> map = new ConcurrentHashMap<Long, UserDO>();
- private Object mutex = new Object();
- /**
- * 取使用者資料,先從快取中取,快取中沒有再從DB取
- * @param userId
- * @return
- */
- public UserDO getUserDO(Long userId) {
- UserDO userDO = map.get(userId);
- if(userDO == null) { ① check
- synchronized(mutex) { ② lock
- if (!map.containsKey(userId)) { ③ check
- userDO = getUserFromDB(userId); ④ act
- map.put(userId, userDO);
- }
- }
- }
- if(userDO == null) { ⑤
- userDO = map.get(userId);
- }
- return userDO;
- }
- private UserDO getUserFromDB(Long userId) {
- // TODO Auto-generated method stub
- return null;
- }
- }
三種做法:
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!