双重检查锁,原来是这样演变来的,你了解吗

最近在看Nacos的源代码时,发现多处都使用了“双重检查锁”的机制,算是非常好的实践案例。这篇文章就着案例来分析一下双重检查锁的使用以及优势所在,目的就是让你的代码格调更加高一个层次。

同时,基于单例模式,讲解一下双重检查锁的演变过程。

Nacos中的双重检查锁

在Nacos的InstancesChangeNotifier类中,有这样一个方法:

private final Map<String, ConcurrentHashSet<EventListener>> listenerMap = new ConcurrentHashMap<String, ConcurrentHashSet<EventListener>>();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<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);
}

该方法的主要功能就是对监听器事件进行注册。其中注册的事件都存在成员变量listenerMap当中。listenerMap的数据结构是key为String,value为ConcurrentHashSet的Map。也就是说,一个key对应一个集合。

针对这种数据结构,在多线程的情况下,Nacos处理流程如下:

  • 通过key获取value值;

  • 判断value是否为null;

  • 如果value值不为null,则直接将值添加到Set当中;

  • 如果为null,就需要创建一个ConcurrentHashSet,在多线程时,有可能会创建多个,因此要使用锁。

  • 通过synchronized锁定一个Object对象;

  • 在锁内再获取一次value值,如果依然是null,则进行创建。

  • 进行后续操作。

上述过程,在锁定前和锁定之后,做了两次判断,因此称作”双重检查锁“。使用锁的目的就是避免创建多个ConcurrentHashSet。

Nacos中的实例稍微复杂一下,下面以单例模式中的双重检查锁的演变过程。

未加锁的单例

这里直接演示单例模式的懒汉模式实现:

public class Singleton {private static Singleton instance;private Singleton() {}public Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}    
}

这是一个最简单的单例模式,在单线程下运转良好。但在多线程下会出现明显的问题,可能会创建多个实例。

以两个线程为例:

可以看到,当两个线程同时执行时,是有可能会创建多个实例的,这很明显不符合单例的要求。

加锁单例

针对上述代码的问题,很直观的想到是进行加锁处理,实现代码如下:

public class Singleton {private static Singleton instance;private Singleton() {}public synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;}
}

与第一个示例唯一的区别是在方法上添加了synchronized关键字。这时,当多个线程进入该方法时,需要先获得锁才能进行执行。

通过在方法上添加synchronized关键字,看似完美的解决了多线程的问题,但却带了性能问题。

我们知道使用锁会导致额外的性能开销,对于上面的单例模式,只有第一次创建时需要锁(防止创建多个实例),但查询时是不需要锁的。

如果针对方法进行加锁,每次查询也要承担加锁的性能损耗。

双重检查锁

针对上面的问题,就有了双重检查锁,示例如下:

public class Singleton {private static Singleton instance;private Singleton() {}public Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

第一,将锁的范围缩小的方法内;

第二,锁之前先判断一下是不是null,如果不为null,说明已经实例化了,直接返回,没必要进行创建;

第三,如果为null,进行加锁,然后再次判断是否为null。为什么要再次判断?因为一个线程判断为null之后,另外一个线程可能已经创建了对象,所以在锁定之后,需要再次核实一下,真的为null,则进行对象创建。

改进之后,既保证了线程的安全性,又避免了锁导致的性能损失。问题到此结束了吗?并没有,继续往下看。

JVM的指令重排

在某些JVM当中,编译器为了性能问题,会进行指令重排。在上述代码中new Singleton()并不是原子操作,有可能会被编译器进行重排操作。

创建对象可抽象为三步:

memory = allocate();    //1:分配对象的内存空间 
ctorInstance(memory);  //2:初始化对象 
instance = memory;     //3:设置instance指向刚分配的内存地址

上面操作中,操作2依赖于操作1,但操作3并不依赖于操作2。因此,JVM是可以进行指令重排优化的,可能会出现如下的执行顺序:

memory = allocate();    //1:分配对象的内存空间 
instance = memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory);  //2:初始化对象

指令重排之后,将操作3的赋值操作放在了前面,那就会出现一个问题:当线程A执行完步骤赋值操作,但还未执行对象初始化。此时,线程B进来了,在第一层判断时发现Instance已经有值了(实际上还未初始化),直接返回对应的值。那么,程序在使用这个未初始化的值时,便会出现错误。

针对此问题,可在instance上添加volatile关键字,使得instance在读、写操作前后都会插入内存屏障,避免重排序。

最终,单例模式实现如下:

public class Singleton {private static volatile Singleton instance;private Singleton() {}public Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

至此,一个完善的单例模式实现了。此时,你是否有一个疑问,为什么Nacos中的双重检查锁没有使用volatile关键字呢?

答案很简单:上面单例模式如果出现指令重排,会导致单例实例被使用。那么,再看Nacos的代码,由于创建ConcurrentHashSet并不会影响到查询,而真正影响查询的是listenerMap.put方法,而ConcurrentHashSet本身是线程安全的。因此,也就不会出现线程安全问题,不用使用volatile关键字了。

小结

阅读源码最有意思的一个地方就是可以看到很多经典知识的实践,如果能够深入思考,拓展一下,会获得意想不到的收获。

再回顾一下本文的重点:

  • 阅读Nacos源码,发现双重检查锁的使用;

  • 未加锁单例模式使用,会创建多个对象;

  • 方法上加锁,导致性能下降;

  • 代码内局部加锁,双重判断,既满足线程安全,又满足性能需求;

  • 单例模式特例:创建对象分多步,会出现指令重排现象,采用volatile进行避免指令重排;

最后,想学习更多类似干货,关注一下吧,持续输出。


往期推荐

ReentrantLock 中的 4 个坑!


synchronized 中的 4 个优化,你知道几个?


synchronized 加锁 this 和 class 的区别!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544621.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WakaTime 记录你的时间(Moana 自动同步信息客户端)

X、写在前面 代码界有一神器&#xff0c;可以记录敲代码的时间&#xff0c;项目名称&#xff0c;编译器等信息&#xff0c;可以极大的满足程序员的虚荣心&#xff0c;它就是 WakaTime 网站链接 WakaTime 可以记录敲代码时间&#xff0c;和具体编辑的文件等信息&#xff0c;并…

图解:为什么非公平锁的性能更高?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在 Java 中 synchronized 和 ReentrantLock 默认使用的都是非公平锁&#xff0c;而它们采用非公平锁的原因都是一致的&#…

死锁的 4 种排查工具 !

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone死锁&#xff08;Dead Lock&#xff09;指的是两个或两个以上的运算单元&#xff08;进程、线程或协程&#xff09;&#xff0c;都在等待…

【HM】第2课:JavaScript基础

<pre>day02第一天的内容&#xff1a;*html标签里面的表单标签*html标签里面的表格标签思维导图1、JavaScript的简介* 什么是JavaScript&#xff1a;js是一个基于对象和事件驱动的语言&#xff0c;应用客户端。**基于对象&#xff1a;在java里面如果使用对象需要创建&…

你没有见过的 7 种 for 循环优化,超好用!

来源&#xff1a;blog.csdn.net/csdn_aiyang/article/details/75162134我们都经常使用一些循环耗时计算的操作&#xff0c;特别是for循环&#xff0c;它是一种重复计算的操作&#xff0c;如果处理不好&#xff0c;耗时就比较大&#xff0c;如果处理书写得当将大大提高效率&…

死锁终结者:顺序锁和轮询锁!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone死锁&#xff08;Dead Lock&#xff09;指的是两个或两个以上的运算单元&#xff08;进程、线程或协程&#xff09;&#xff0c;都在等待…

轮询锁使用时遇到的问题与解决方案!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone当我们遇到死锁之后&#xff0c;除了可以手动重启程序解决之外&#xff0c;还可以考虑是使用顺序锁和轮询锁&#xff0c;这部分的内容可以…

16 条 yyds 的代码规范

作者 | 涛姐涛哥链接 | cnblogs.com/taojietaoge/p/11575376.html背景&#xff1a;如何更规范化编写Java 代码的重要性想必毋需多言&#xff0c;其中最重要的几点当属提高代码性能、使代码远离Bug、令代码更优雅。一、MyBatis 不要为了多个查询条件而写 1 1当遇到多个查询条件…

C# 导出word文档及批量导出word文档(3)

在初始化WordHelper时&#xff0c;要获取模板的相对路径。获取文档的相对路径多个地方要用到&#xff0c;比如批量导出时要先保存文件到指定路径下&#xff0c;再压缩打包下载&#xff0c;所以专门写了个关于获取文档的相对路径的类。 1 #region 获取文档的相对路径2 pub…

再见收费的 XShell,我改用国产良心工具!

使用或维护Linux系统的都知道&#xff0c;我们日常对服务器的操作&#xff0c;一般都会借助SSH工具远程登录到服务器之后进行操作。常用的SSH工具有不少&#xff0c;比如&#xff1a;Xshell、Putty、SSH Secure Shell Client、secureCRT等等。我使用过其中两种secureCRT和Xshel…

全球六大国际域名解析量统计报告(6月25日)

IDC评述网&#xff08;idcps.com&#xff09;06月29日报道&#xff1a;根据DailyChanges公布的实时数据显示&#xff0c;在2015年6月25日&#xff0c;全球六大国际域名解析量总量持续攀升至153,246,819个&#xff0c;环比6月16日&#xff0c;净增46,078个&#xff0c;涨幅增大3…

Windows 创建符号链接

一、场景分析 1.环境变量 在Windows系统配置 环境变量 的时候&#xff0c;经常会遇到以下 路径 情况&#xff1a; C:\Program Files C:\Program Files (x86)\Common Files2.异常情况 这种路径中&#xff0c;存在空格字符&#xff0c;在一些程序调用时&#xff0c;可能出现异…

1.3w字,一文详解死锁!

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone死锁&#xff08;Dead Lock&#xff09;指的是两个或两个以上的运算单元&#xff08;进程、线程或协程&#xff09;&#xff0c;都在等待…

PHP与ThinkPHP读写文件

2019独角兽企业重金招聘Python工程师标准>>> 使用php将数据写入到指定的文件 $str"<?php return".var_export($phiz,true)."?>"; file_put_contents(./Data/phiz.php); 使用php读取指定的文件 …

【图解】透彻Java线程状态转换

大家好&#xff0c;我是阿星&#xff0c;好久不见&#xff0c;欢迎来到Java并发编程系列番外篇线程状态转换&#xff0c;内容通俗易懂&#xff0c;请放心食用。线程状态先来个开场四连问Java线程状态有几个&#xff1f;Java线程状态是如何转换&#xff1f;Java线程状态转换什么…

CentOS7安装Hadoop2.7完整流程

2019独角兽企业重金招聘Python工程师标准>>> 1、环境&#xff0c;3台CentOS7&#xff0c;64位&#xff0c;Hadoop2.7需要64位Linux&#xff0c;CentOS7 Minimal的ISO文件只有600M&#xff0c;操作系统十几分钟就可以安装完成&#xff0c; Master 192.168.0.182 Slav…

如果不这样用,Nacos也有安全问题!

前言配置管理作为软件开发中重要的一环&#xff0c;肩负着连接 代码和环境的职责&#xff0c;能很好的分离开发人员和维护人员的关注点。Nacos 的配置管理功能就很好地满足了云原生应用对于配置管理的需求&#xff1a;既能做到配置和代码分离&#xff0c;也能做到配置的动态…

聊聊Spring事务失效的12种场景,太坑了

前言对于从事java开发工作的同学来说&#xff0c;spring的事务肯定再熟悉不过了。在某些业务场景下&#xff0c;如果一个请求中&#xff0c;需要同时写入多张表的数据。为了保证操作的原子性&#xff08;要么同时成功&#xff0c;要么同时失败&#xff09;&#xff0c;避免数据…

什么是可中断锁?有什么用?怎么实现?

作者 | 王磊来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone在 Java 中有两种锁&#xff0c;一种是内置锁 synchronized&#xff0c;一种是显示锁 Lock&#xff0c;其中 Lock 锁是可中断锁&#xff…

10个经典又容易被人疏忽的JVM面试题

前言整理了10个经典又容易被疏忽的JVM面试题&#xff0c;谢谢阅读&#xff0c;大家加油哈.1. 对象一定分配在堆中吗&#xff1f;有没有了解逃逸分析技术&#xff1f;「对象一定分配在堆中吗&#xff1f;」 不一定的&#xff0c;JVM通过「逃逸分析」&#xff0c;那些逃不出方法的…