并发情况下,单例模式之双重检验锁陷阱
在我前面有写过一篇关于单例模式的几种创建的文章,最近在看多线程的时候,发现如果使用双重检验锁则可能会发生问题,接下来看我细细道来
单例模式的几种创建方式文章地址:https://www.jianshu.com/p/8ec72e016275
首先看一段代码
public class SingletonV4 {
private static SingletonV4 singletonV4;
private SingletonV4() {
System.out.println("--初始化--");
}
/**
* 双重检验锁
* 能够保证线程安全,且效率高
* @return
*/
public static SingletonV4 getInstance() {
if (null == singletonV4) {
synchronized (SingletonV4.class) {
if (null == singletonV4) {
singletonV4 = new SingletonV4();
}
}
}
return singletonV4;
}
public static void main(String[] args) {
SingletonV4 instance1 = SingletonV4.getInstance();
SingletonV4 instance2 = SingletonV4.getInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
--初始化--
com.dream.sunny.SingletonV4@1716361
com.dream.sunny.SingletonV4@1716361
如上是一段单例模式中的懒汉模式双重检验锁,可能会有所疑惑,为什么需要两次if判断才进行初始化对象 第一次if判断主要是为了减少性能开销,之所以这么说,如果不加第一个if判断,每次进入getInstance()方法,synchronized关键字会将整个代码进行锁住,加锁操作,在进行判断是否已经初始化,在进行释放锁,加锁和释放锁是有较大的性能开销,所以在最外层包裹一层if判断实例是否被初始化,这样就不会每次加锁和释放锁了
既然synchronized锁增加了性能开销,为什么要加锁呢 当然在单线程情况下,是没有必要加锁,而多线程情况下,多个线程同时进行初始化对象操作,这样就会有线程安全性问题,为了防止这种情况,我们需要使用synchronized,这样该方式在多线程情况下就是线程安全的
第二次if判断目的在于有可能其他线程获取过锁,已经初始化改变量。第二次检查还未通过,才会真正初始化变量。 这个方法检查判定两次,并使用锁,所以形象称为双重检查锁定模式。 这个方案缩小锁的范围,减少锁的开销,看起来很完美。然而这个方案有一些问题却很容易被忽略。
问题点:
这个被忽略的问题在于 singletonV4 = new SingletonV4();
在java中创建一个对象并非是一个原子操作,可以查看如下字节码代码
#创建一个新对象(创建 SingletonV4 对象实例,分配内存)
19: new #6 // class com/dream/sunny/SingletonV4
#复制栈顶部一个字长内容(复制栈顶地址,并再将其压入栈顶)
22: dup
#根据编译时类型来调用实例方法(调用构造器方法,初始化 SingletonV4 对象)
23: invokespecial #7 // Method "<init>":()V
#设置类中静态字段的值
26: putstatic #5 // Field singletonV4:Lcom/dream/sunny/SingletonV4;
#从局部变量0中装载引用类型值(存入局部方法变量表)
29: aload_0
从字节码中可以看到创建一个对象实例,大致可以分为以下几步:
1.创建对象并分配内存地址
2.调用构造器方法,执行初始化对象
3.将对象的引用地址赋值给变量
在多线程情况下,上面三个步骤可能会发生指令重排(在一些JIT编译器中),编译器或处理器会为了提高代码性能效率,而改变代码的执行顺序。 上面三个步骤2和3之间可能会发生重排,但是1不会,因为2和3是要依托1指令的执行结果,才能继续往下走:
1.创建对象并分配内存地址
2.将对象的引用地址赋值给变量
3.调用构造器方法,执行初始化对象
Java 语言规规定了线程执行程序时需要遵守 intra-thread semantics。 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。 这个重排序在没有改变单线程程序的执行结果的前提下,可以提高程序的执行性能。 虽然重排序并不影响单线程内的执行结果,但是在多线程的环境就带来一些问题。
模拟两个线程创建单例的场景,如下:
时间 |
线程A |
线程B |
---|---|---|
t1 |
创建对象 |
~ |
t2 |
分配内存地址 |
~ |
t3 |
~ |
判断对象是否为空 |
t4 |
~ |
对象不为空,访问该对象 |
t5 |
初始化对象 |
~ |
t6 |
访问该对象 |
~ |
如果线程A获取到锁,进入到创建对象实例,这个时候发生了指令重排,线程A执行到t3时刻,此时线程B抢占了CPU执行时间片,但是由于此时对象不为空,则直接返回对象出去,然而使用该对象却发现该对象未被初始化就会报错,并且从始至终,线程B无需获取锁
指令重排 前面已经分析到,出现错误的原因在于“指令重排”,那什么是指令重排呢?它什么在并发情况下指令重排会直接影响到程序的执行结果呢?首先我们看一下“顺序一致性内存模型”概念。
顺序一致性理论内存模型 顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:
- 一个线程中的所有操作必须按照程序的顺序来执行。
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
实际JMM模型概念 但是,顺序一致性模型只是一个理想化了的模型,在实际的JMM实现中,为了尽量提高程序运行效率,和理想的顺序一致性内存模型有以下差异: 在顺序一致性模型中,所有操作完全按程序的顺序串行执行。在JMM中不保证单线程操作会按程序顺序执行(即指令重排序)。 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。 顺序一致性模型保证对所有的内存写操作都具有原子性,而JMM不保证对64位的long型和double型变量的读/写操作具有原子性(分为2个32位写操作进行,本文无关不细阐述)
什么是指令重排序 指令重排序是指编译器或处理器为了优化性能而采取的一种手段,在不存在数据依赖性情况下(如写后读,读后写,写后写),调整代码执行顺序。 举个例子:
int a = 1;
int b = 10;
int c = a * b
这段代码C依赖于A,B,但A,B没有依赖关系,所以代码可能有2种执行顺序:
- A->B->C
- B->A->C 但无论哪种最终结果都一致,这种满足单线程内无论如何重排序不改变最终结果的语义,被称作as-if-serial语义,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉: 单线程程序是按程序的顺序来执行的。
双重检验锁问题解决方案 回头看下我们出问题的双重检查锁程序,它是满足as-if-serial语义的吗?是的,单线程下它没有任何问题,但是在多线程下,会因为重排序出现问题。 解决方案就是volatile关键字,对于volatile我们最深的印象是它保证了”可见性“,它的”可见性“是通过它的内存语义实现的:
- 写volatile修饰的变量时,JMM会把本地内存中值刷新到主内存
- 读volatile修饰的变量时,JMM会设置本地内存无效
重点:为了实现可见性内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来防止重排序!
volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用
- 保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。
- 禁止指令重排序优化。
由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
注意,volatile禁止指令重排序在 JDK 5 之后才被修复
对之前代码加入volatile关键字,即可实现线程安全的单例模式。
public class SingletonV4 {
private static volatile SingletonV4 singletonV4;
private SingletonV4() {
System.out.println("--初始化--");
}
/**
* 双重检验锁
* 能够保证线程安全,且效率高
* @return
*/
public static SingletonV4 getInstance() {
if (null == singletonV4) {
synchronized (SingletonV4.class) {
if (null == singletonV4) {
singletonV4 = new SingletonV4();
}
}
}
return singletonV4;
}
public static void main(String[] args) {
SingletonV4 instance1 = SingletonV4.getInstance();
SingletonV4 instance2 = SingletonV4.getInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
--初始化--
com.dream.sunny.SingletonV4@1716361
com.dream.sunny.SingletonV4@1716361
总结 对象的创建可能发生指令的重排序,使用 volatile 可以禁止指令的重排序,保证多线程环境内的系统安全。
参考博客:https://www.cnblogs.com/lkxsnow/p/12293791.html
- Linux系统下的ssh使用(依据个人经验总结)
- 从MapX到MapXtreme2004[4]-标注AutoLabel
- Linux下锁定账号,禁止登录系统的设置总结
- 深度解析 TypeConverter & TypeConverterAttribute (一)
- 从MapX到MapXtreme2004[7]-对Table、Feature等的理解
- 互联网赋能传统装企 “科技撬动力巨大”
- Python接口自动化-8-测试报告
- http应用优化和加速说明-负载均衡
- linux负载均衡总结性说明(四层负载/七层负载)
- 从MapX到MapXtreme2004[6]-标点心得
- silverlight3中的"伪"3D
- 暴利驱动的疯狂游戏“外挂”:非法获利可达数百万;X-Agent 后门大升级,俄罗斯 APT28 间谍活动更为隐蔽
- Nginx+keepalived双机热备(主从模式)
- 即使不做程序员,也要学会像程序员一样去思考
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Python json数据爬取处理,红点官网大奖设计作品爬取
- 斗图狂魔必备沙雕表情包,python多线程爬取斗图啦表情图片
- 5个基本Linux命令行工具的现代化替代品
- Chrome 84 正式发布,支持私有方法、用户空闲检测!
- 类及数据库的应用,G-MARK网站数据Python爬虫系统的构建
- 获取素材图无忧,Pixabay图库网Python多线程采集下载
- Python关键词数据采集案例,5118查询网站关键词数据采集
- Python结巴分词,字符串余弦相似度算法实现关键词筛选及整理
- git的分支远程连接和远程分支的拉取推送及冲突处理
- requests session的应用,python金点设计奖数据爬虫
- 站长工具关键词挖掘采集,Python关键词批量挖掘采集工具
- python百度关键词相关搜索词采集,链轮查询采集exe工具
- Python最火爬虫框架Scrapy入门与实践,豆瓣电影 Top 250 数据采集
- Python爬虫三种解析方式,Pyhton360搜索排名查询
- Python关键词百度指数采集,抓包Cookie及json数据处理