双重检查锁,原来是这样演变来的,你了解吗
最近在看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 的区别!
双重检查锁,原来是这样演变来的,你了解吗相关推荐
- java双重检查锁单例真的线程安全吗?
相信大多数同学在面试当中都遇到过手写单例模式的题目,那么如何写一个完美的单例是面试者需要深究的问题,因为一个严谨的单例模式说不定就直接决定了面试结果,今天我们就要来讲讲看似线程安全的双重检查锁单例模 ...
- Java中的双重检查锁(double checked locking)
起因 在实现单例模式时,如果未考虑多线程的情况,很容易写出下面的代码(也不能说是错误的): public class Singleton {private static Singleton uniqu ...
- 双重检查锁模式导致空指针
今天遇到一个问题:莫名奇妙报了个空指针,后来发现原来单例模式在高并发下引起的: 双重检查锁模式的一般实现: 双重检查锁模式解决了单例.性能.线程安全问题,但是这种写法同样存在问题:在多线程的情况下,可 ...
- java 双重检查锁_Java中可怕的双重检查锁定习惯用法
java 双重检查锁 本文讨论的问题不是新问题,但即使是经验丰富的开发人员也仍然很棘手. 单例模式是常见的编程习惯用法. 但是,当与多个线程一起使用时,必须进行某种类型的同步,以免破坏代码. 在相关文 ...
- java 双重检查锁 有序_Java中的双重检查锁(double checked locking)
1 public classSingleton {2 private staticSingleton uniqueSingleton;3 4 privateSingleton() {5 }6 7 pu ...
- 单例模式之双重检查锁(double check locking)的发展历程
不安全的单例 没有注意过多线程安全问题的时候,我们的单例可能是这样的: public final class Singleton {private static Singleton instance; ...
- java并发编程(二十六)——单例模式的双重检查锁模式为什么必须加 volatile?
前言 本文我们从一个问题出发来进行探究关于volatile的应用. 问题:单例模式的双重检查锁模式为什么必须加 volatile? 什么是单例模式 单例模式指的是,保证一个类只有一个实例,并且提供一个 ...
- 双重检查锁与单例模式
单例模式是比较常见的一种设计模式,在开发实践中经常看到它的身影,它有很多种实现方式,曾经有人在一篇文章中列举了十几种实现方式,比如饿汉式.懒汉式.双重检查锁.枚举...等等,程序员应该都熟悉这些常见的 ...
- 双重检查锁(Double-Checked Locking)的缺陷
双重检查锁(Double-Checked Locking)的缺陷 第一种有问题的写法 第二种有问题的写法 第三种有问题的写法 它不起作用 它不起作用的第一个原因 一个测试用例显示它不起作用 一个不起作 ...
最新文章
- OpenNMS Log Correlator
- Java医疗管理系统技术描述
- HyperLedger Fabric Introduction——区块链超级账本介绍
- 图解 CSS (9): 列表
- asp mysql添加数据_ASP:ado.net 实例向数据库添加数据。
- python win32ui_Python创建普通菜单示例【基于win32ui模块】
- LeetCode 542 01 矩阵
- java包管理之gradle安装
- 联合概率分布的学习笔记
- 一个队列类的实现(比delphi自带的速度快70倍)
- JSON文件导入Unity3d中是空的的问题
- 张宇:7~12月考研数学该如何复习?
- Deep domain generalization combining a priori diagnosis knowledge阅读笔记
- C# 判断是不是非负数
- PBR基本原理和概念以及PBR流程
- 一维连续型随机变量的函数分布
- ALG 求单峰的位置
- 某招聘网站“数据分析”相关岗位招聘信息爬取并分析
- 网络应用程序的通信视角
- HAL层,.sensors.h 头文件分析
热门文章
- aix升级新安装oracle,安装Oracle 11gR2 AIX 5.3 升级到TL11的一些小记录
- 获取http地址如何从上面抓取图片_用 Python 自动抓取妹子图
- 利用python进行数据分析D2——ch03IPython
- MySQL5.7.17的简单配置文件
- java虚拟机规范阅读(三)异常
- TYVJ P1012 火柴棒等式 Label:枚举
- js ‘use strict’详解
- 介绍MFSideMenu左右滑动控件的使用
- 将Java应用程序本地编译为EXE的几种方法
- SQL SERVER重置自动编号列(标识列)