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


highlight: a11y-dark

theme: condensed-night-purple

引言

大家好,我是有清

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

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

private final Map > listenerMap = new ConcurrentHashMap >();

private final Object lock = new Object();

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

```

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

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?它的优势在哪里?一步步看起来👇

006APoFYly8h7xqp8e7izg308w08we81

单例模式

创建

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

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

小林心里窃笑:“好家伙,这不撞我枪口上了,刚学的单例模式就能用上了” 006APoFYly1gokwfouh08g30at0ayh13 刷刷两下,代码完毕 ```Java

public class OrganTree {

private static OrganTree uniqueOrganTree;  

private OrganTree() { }  

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

Pasted image 20230204224425.png

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

加锁

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

private static OrganTree uniqueOrganTree;  

private OrganTree() { }  

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

} ```

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

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

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

双重检查锁

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

public class OrganTree {

private static OrganTree uniqueOrganTree;  

private OrganTree() { }  

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

}

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

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

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

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

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

Pasted image 20230204230926.png

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

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

```Java

public class OrganTree {

private volatile static OrganTree uniqueOrganTree;  

private OrganTree() { }  

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

}

```

额外思考

除了该种双重检查锁模式可以安全生成单例 bean,那么还有什么方法呢?对,就是 CAS,在有并发但是并发程度不是非常高的情况下,这种模式可以进行考虑 ```Java private static AtomicReference< OrganTree> OrganTreeAtomicReference = new AtomicReference< OrganTree>();

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

总结

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

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

闲言碎语

周六和朋友一起去看了满江红,

在结尾,秦桧带领满城大军一起朗诵《满江红》,我还在想这不是给秦桧洗白吧

后来出现了假秦桧,这一切似乎就能说通了,在电影院我只觉得,哦,对这样剧情才合理,不能给秦桧洗白了,只有假秦桧才会这样激情慷慨的去背诵《满江红》,真秦桧如此痛恨岳飞,我想宁可死也不愿意在满城大军前背诵岳飞的《满江红》吧

仔细一想,或许这个假秦桧其实也是真秦桧刚入朝的时候,那个时候可能也是一个想着为天下立心,为生民立命的好臣子,然而欲望的洪流还是打破了内心的屏障,尽管他自己说是为了休养生息而不是叛敌卖国,然而面具戴久了,可能真成了自己的脸了

这个结尾其实有点蓦然把自己拉回《让子弹飞》的黄四郎,在黄四郎死后,张麻子恍惚间仿佛在火车上又看到了黄四郎,那么,在现在的火车上还有秦桧吗。

Snipaste<em>2023-02-05</em>15-02-42

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值