你的双重检查锁真的锁住了对象么?

引言

一位名人说过:检查对象,一次不够,得两次

基于这位名人的话,不少框架都使用了双重检查锁去获取对象 比如 在 Nacos 的 InstancesChangeNotifier 类中,eventListeners 这个对象的获取


private final Map<String, ConcurrentHashSet<EventListener>> listenerMap = new ConcurrentHashMap<String, ConcurrentHashSet<EventListener>>();

private final Object lock = newObject();

public void registerListener(String groupName, String serviceName, String clusters, EventListener listener) {
    String key = ServiceInfo.getKey(NamingUtils.getGroupedName(serviceName, groupName), clusters);
    ConcurrentHashSet<EventListener> eventListeners = listenerMap.get(key);
    if (eventListeners == null) {
        synchronized (lock) {
            eventListeners = listenerMap.get(key);
            if (eventListeners == null) {
                eventListeners = new ConcurrentHashSet<EventListener>();
                listenerMap.put(key, eventListeners);
            }
        }
    }
    eventListeners.add(listener);
}

再或者在 Spring 获取单例 Bean 的时候

protected Object getSingleton(String beanName, boolean allowEarlyReference){  
   // Quick check for existing instance without full singleton lock  
   Object singletonObject = this.singletonObjects.get(beanName);  
   if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {  
      singletonObject = this.earlySingletonObjects.get(beanName);  
      if (singletonObject == null && allowEarlyReference) {  
         synchronized (this.singletonObjects) {  
            // Consistent creation of early reference within full singleton lock  
            singletonObject = this.singletonObjects.get(beanName);  
            if (singletonObject == null) {  
               singletonObject = this.earlySingletonObjects.get(beanName);  
               if (singletonObject == null) {  
                  ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);  
                  if (singletonFactory != null) {  
                     singletonObject = singletonFactory.getObject();  
                     this.earlySingletonObjects.put(beanName, singletonObject);  
                     this.singletonFactories.remove(beanName);  
                  }  
               }  
            }  
         }  
      }  
   }  
   return singletonObject;  
}

通过上面两个例子再结合双重检查锁的这个名字,我们不难得出,双重检查锁,就是判断一个对象在不在,不在的话,先锁住这个对象,再判断一次在不在,不在的话就开始加载这个对象

那么为什么框架都喜欢使用这种双重检查锁的模式去获取 Bean?它的优势在哪里?一步步看起来👇

单例模式

创建

最近刚学了单例模式的小林同学,非常的飘,他觉得高低得在项目中使用一下单例模式,展示一下自己的功力

这不刚巧接了个需求,技术经理和小王说:“你小子,这边写的时候注意一下,这个机构树全局只能有一个实例存在”

小林心里窃笑:“好家伙,这不撞我枪口上了,刚学的单例模式就能用上了”

刷刷两下,代码完毕

publicclassOrganTree{  

    privatestatic OrganTree uniqueOrganTree;  

    privateOrganTree(){ }  

    public OrganTree getInstance(){  
        if (null == uniqueOrganTree) {  
            uniqueOrganTree = new OrganTree();  
        }  
        return uniqueOrganTree;  
    }  
}

技术经理一看小林写的这个代码:咋们这是个大项目,多线程情况下,你这个单例模式会有用么?小林默默思考了起来

假设现在有两个线程,线程A和线程B

1、线程 A 率先出发,检查到 uniqueOrganTree 为空,准备 new 出来的时候,但是爱情有先后,但是线程没有先后(线程具有时间片的概念,线程之间可能会发生轮转执行)

2、线程 B 抢断,检查到 uniqueOrganTree 为空,立马初始化对象,返回了 uniqueOrganTree@1234

3、这时候 A 幡然醒悟,开始 new 动作,返回了 uniqueOrganTree@78910

这个时候好好的单例模式,在多线程下,就变成多例了

加锁

小林心想:技术经理还得是技术经理,那我加个锁,不就完事了 刷刷两下,代码完毕

publicclassOrganTree{  

    privatestatic OrganTree uniqueOrganTree;  

    privateOrganTree(){ }  

    publicsynchronized OrganTree getInstance(){  
        if (null == uniqueOrganTree) {  
            uniqueOrganTree = new OrganTree();  
        }  
        return uniqueOrganTree;  
    }  
}

技术经理一看:synchronized 虽然可以保证只能有一个线程进入加载 uniqueOrganTree 的流程,

尽管 synchronized 做过代码优化,但是还是具有性能开销,并且我们只有在第一次初始化的时候才用到 synchronized,后面就没必要使用了

那么这个时候我们就需要使用到双重检查锁

双重检查锁

我们双重检查锁的目的就是降低这个生成对象的时候的性能消耗,只有在准备初始化对象的时候才去使用锁 那么我们代码可以改成这样

publicclassOrganTree{  

    privatestatic OrganTree uniqueOrganTree;  

    privateOrganTree(){ }  

    publicsynchronized OrganTree getInstance(){  
        if (null == uniqueOrganTree) {  
            synchronized (OrganTree.class) {  
                if (null == uniqueOrganTree) {  
                    uniqueOrganTree = new OrganTree();  
                }  
            }  
        }  
        return uniqueOrganTree;  
    }  
}

这样是否就万无一失呢?我们很稳妥的加锁,检查两次

我们仔细想一下初始化对象具体可以分为哪几步?分配内存空间 -> 初始化对象 -> 将对象指向刚才分配的内存空间

在编译器的优化下,这个步骤可能会变为 分配内存空间 -> 将对象指向刚才分配的内存空间 -> 初始化对象

可能会有同学质疑了,你说的不对,这样不都乱掉了,加载对象,但其实编译器的优化都遵循 as-if-serial 语义,在单线程下无论怎么排序,都不能影响结果

考虑这个优化,然后在多线程下,我们的步骤可以具体分为

那么,我们如何打破这种重排序、并且使这两个线程之间互通有无呢?就需要借助到我们的 volatile

什么你不知道 volatile,赶快去学起来吧,金三银四必需品,我们利用 volatile 重新写一遍,使用了volatile 关键字后,重排序被禁止(线程可以访问到的时候肯定是初始化后的对象),保证可见性(线程地址北刷新后强制刷新回主内存)

publicclassOrganTree{  

    privatevolatilestatic OrganTree uniqueOrganTree;  

    privateOrganTree(){ }  

    publicstatic OrganTree getInstance(){  
        if (null == uniqueOrganTree) {  
            synchronized (OrganTree.class) {  
                if (null == uniqueOrganTree) {  
                    uniqueOrganTree = new OrganTree();  
                }  
            }  
        }  
        return uniqueOrganTree;  
    }  
}

额外思考

除了该种双重检查锁模式可以安全生成单例 bean,那么还有什么方法呢?对,就是 CAS,在有并发但是并发程度不是非常高的情况下,这种模式可以进行考虑

privatestatic AtomicReference< OrganTree> OrganTreeAtomicReference = new AtomicReference< OrganTree>();  

publicstatic  OrganTree getInstanceByCAS(){  
    for (; ; ) {  
         OrganTree  OrganTree =  OrganTreeAtomicReference.get();  
        if ( OrganTree != null) {  
            return  OrganTree;  
        }  
         OrganTree = new  OrganTree();  
        if ( OrganTreeAtomicReference.compareAndSet(null,  OrganTree)) {  
            return  OrganTree;  
        }  
    }  
}

总结

双重检查锁其实就是一种用来减少软件并发系统中竞争和同步的开销。双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。它通常用于减少加锁开销,尤其是为多线程环境中的单例模式实现懒加载

在我们的项目环境中,使用该模式的优点就在于搭配单例模式,可以为对象提供一个全局访问点,并且大大减少 synchroized 所带来的性能消耗(当然这种单例模式还不是绝对安全的)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值