Java 中的Double Check Lock


对于多线程编程来说,同步问题是我们需要考虑的最多的问题,同步的锁什么时候加,加在哪里都需要考虑,当然在不影响功能的情况下,同步越少越好,锁加的越迟越优是我们都必须认同的。DCLDouble Check Lock)就是为了达到这个目的。 

DCL简单来说就是check-lock-check-act,先检查再锁,锁之后再检查一次,最后才执行操作。这样做的目的是尽可能的推迟锁的时间。网上普遍举的一个例子是延迟加载的例子。

Java代码

public   class  LazySingleton {  

    private   static   volatile  LazySingleton instance;  

      

    public   static  LazySingleton getInstantce() {  

        if  (instance ==  null ) {  

            synchronized  (LazySingleton. class ) {  

                if  (instance ==  null ) {  

                    instance = new  LazySingleton();  

                }  

10             }  

11         }  

12         return  instance;  

13     }  

14 


对上面的例子来说,我们当然也可以把锁加在方法上,那样的话每次获取实例都需 要获取锁,但其实对这个instance来说,只有在第一次创建实例的时候才需要同步,所以为了减少同步,我们先check了一下,看看这个 instance是否为空,如果为空,表示是第一次使用这个instance,那就锁住它,new一个LazySingleton的实例,下次另一个线程来 getInstance的时候,看到这个instance不为空,就表示已经创建过一个实例了,那就可以直接得到这个实例,避免再次锁。这是第一个 check的作用。 

第二个check是解决锁竞争情况下的问题,假设现在两个线程来请求getInstanceAB线程同时发现instance为空,因为我们 在方法上没有加锁,然后A线程率先获得锁,进入同步代码块,new了一个instance,之后释放锁,接着B线程获得了这个锁,发现instance已 经被创建了,就直接释放锁,退出同步代码块。所以这就是check-lock-then check。 
网上有很多文章讨论DCL的失效问题,我就不赘述了,Java5之后可以通过将字段声明为volatile来避免这个问题。 
我推荐一篇很好的文章《用happen-before规则重新审视DCL》,里面讲的非常好。 

上面这个是最简单的例子,网上随处可见,双重检查的使用可不只限于单例的初始化,下面我举个实际使用中的例子。 
缓存用户信息,我们用一个hashmap做用户信息的缓存,keyuserId

Java代码

15 public   class  UserCacheDBService {  

16       

17     private   volatile  Map<Long, UserDO> map =  new  ConcurrentHashMap<Long, UserDO>();  

18     private  Object mutex =  new  Object();  

19   

20     /**  

21      * 取用户数据,先从缓存中取,缓存中没有再从DB  

22      * @param userId  

23      * @return  

24      */   

25     public  UserDO getUserDO(Long userId) {  

26         UserDO userDO = map.get(userId);  

27           

28         if (userDO ==  null ) {                            ① check  

29             synchronized (mutex) {                       ② lock  

30                 if  (!map.containsKey(userId)) {        ③ check  

31                     userDO = getUserFromDB(userId);    ④ act  

32                     map.put(userId, userDO);  

33                 }  

34             }  

35         }  

36           

37         if (userDO ==  null ) {                             ⑤  

38             userDO = map.get(userId);  

39         }  

40           

41         return  userDO;  

42     }  

43   

44     private  UserDO getUserFromDB(Long userId) {  

45         // TODO Auto-generated method stub   

46         return   null ;  

47     }  

48   

49 }  

Java代码   

50 public class UserCacheDBService {  

51       

52     private volatile Map<Long, UserDO> map = new ConcurrentHashMap<Long, UserDO>();  

53     private Object mutex = new Object();  

54   

55     /** 

56      * 取用户数据,先从缓存中取,缓存中没有再从DB 

57      * @param userId 

58      * @return 

59      */  

60     public UserDO getUserDO(Long userId) {  

61         UserDO userDO = map.get(userId);  

62           

63         if(userDO == null) {                            ① check  

64             synchronized(mutex) {                       ② lock  

65                 if (!map.containsKey(userId)) {        ③ check  

66                     userDO = getUserFromDB(userId);    ④ act  

67                     map.put(userId, userDO);  

68                 }  

69             }  

70         }  

71           

72         if(userDO == null) {                             ⑤  

73             userDO = map.get(userId);  

74         }  

75           

76         return userDO;  

77     }  

78   

79     private UserDO getUserFromDB(Long userId) {  

80         // TODO Auto-generated method stub  

81         return null;  

82     }  

83   

84 }  



三种做法: 
1、 没有锁,即没有,当在代码处判断userDO为空之后,直接从DB取数据,这种情况下有可能会造成数据的错误。举个例子,AB两个线程,A线程 需要取用户信息,B线程更新这个user,同时把更新后的数据放入map。在没有任何锁的情况下,A线程在时间上先于B线程,A首先从DB取出这个 user,随后线程调度,B线程更新了user,并把新的user放入map,最后A再把自己之前得到的老的user放入map,就覆盖了B的操作。B以 为自己已经更新了缓存,其实并没有。 

2、 没有第二次check,即没有的情况,在加锁之后立即从DB取数据,这种情况可能会多几次DB操作。同样AB两个线程,都需要取用户信息,AB在进 入代码处时都发现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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值