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

最近在看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,一经查实,立即删除!

相关文章

Linux学习之FTP服务

环境&#xff1a;red hat6.5安装包&#xff1a;vsftp、ftp服务器主配置文件&#xff1a;/etc/vsftpd/vsftpd.conf主配置参数&#xff1a;anonymous_enableYES //&#xff08;默认&#xff09;允许匿名登录anon_upload_enableYES //允许匿名上传文件anon_mkdir_write_enableYES …

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;而它们采用非公平锁的原因都是一致的&#…

java timezone_Java TimeZone setDefault()方法与示例

java timezoneTimeZone类的setDefault()方法 (TimeZone Class setDefault() method) setDefault() method is available in java.util package. setDefault()方法在java.util包中可用。 setDefault() method is used to assign the default time zone which is retrieved by us…

5.2 测试计划和估算

5.2 测试计划和估算 2015-06-23 5.2.2. 测试计划活动&#xff08;K3&#xff09; 对整个系统或部分系统可能的测试计划活动包括&#xff1a; 确定测试的范围和风险&#xff0c;明确测试的目标&#xff1b;定义测试的整体方法&#xff08;测试策略&#xff09;&#xff0c;包括测…

Android 模拟器调试的缺点

1.模拟器调试速度太慢&#xff0c;不能清晰真实反映开发中的问题。 2.安卓定制化现象严重&#xff0c;模拟器达不到真机的真实水平&#xff0c;如控件样式、分辨率。 3.模拟器不能模拟所有的API&#xff0c;如Email、电话、短信、横竖屏、GPS、蓝牙、多点触控、震动、服务等基…

java timezone_Java TimeZone getDefault()方法与示例

java timezoneTimeZone类的getDefault()方法 (TimeZone Class getDefault() method) getDefault() method is available in java.util package. getDefault()方法在java.util包中可用。 getDefault() method is used to return the default time zone for this host. getDefaul…

死锁的 4 种排查工具 !

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

搜索百度百科官方创建入口,怎么创建更新公司的百度百科词条呢?

在百度搜索百度百科找到百度百科官方创建入口&#xff0c;可以上传并创建公司类的百度百科词条&#xff0c;创建词条后还可以再修改更新百科词条&#xff0c;最终完善好的百度百科词条将会在百度上获得大量曝光。那么百度百科可以怎么创建&#xff0c;下面洛希爱做百科网把十多…

【HM】第2课:JavaScript基础

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

Nginx For Windows 关于 worker_connections 不生效问题

○、结论 Nginx For Windows 建议使用 http://nginx-win.ecsds.eu/ 下载 nginx 1.17.0.1 Crow 一、起因 项目中有一个 API 服务&#xff0c;对客户端通信进行支持&#xff0c;大概 1w 客户端&#xff0c;每分钟都会进行通信。 高峰期的时候服务负载较高&#xff0c;为了防…

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

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

java valueof_Java Short类valueOf()方法及示例

java valueofSyntax: 句法&#xff1a; public static Short valueOf (short value);public static Short valueOf (String value);public static Short valueOf (String value, int radixs);简短的类valueOf()方法 (Short class valueOf() method) valueOf() method is avail…

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

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

Nginx For Windows HTTP转发和负载

Nginx For Windows HTTP转发和负载一、需求说明二、配置文件一、需求说明 使用Nginx进行端口转发&#xff0c;并且负载到两台服务器的服务上。 监控本地服务器的 9099 端口&#xff0c;转发并负载到 127.0.0.1:9001 和 127.0.0.1:9002 服务上。&#xff08;可以选择只转发到一…

Java PushbackReader mark()方法与示例

PushbackReader类mark()方法 (PushbackReader Class mark() method) mark() method is available in java.io package. mark()方法在java.io包中可用。 mark() method is used to mark the current position in this stream and this method throws an exception during the ca…

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

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

DHCP 日志分析

DHCP 日志分析 DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09;是一种有效的IP 地址分配手段&#xff0c;现已经被广泛地应用在各种局域网管理。它能动态地向网络中每台计算机分配唯一的IP 地址&#xff0c;并提供安全、可靠、简…

Nginx For Windows Socket 端口转发

Nginx For Windows Socket 端口转发一、需求说明二、配置文件一、需求说明 使用Nginx进行端口转发 Socket 端口通信。 监控本地服务器的 3001 端口&#xff0c;转发到 10.73.60.48:3001 服务器上的 Socket 端口服务。 二、配置文件 完整配置文件如下&#xff0c;测试可用 #…

16 条 yyds 的代码规范

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