DCL单例模式你不知道的秘密
1.什么是单例模式?
当new一个对象时,每一次new都是创建一个新的对象,而单例模式就是无论怎么样去获取一个对象,永远都是拿到的同一个对象。
2.传统懒汉单例模式到DCL单例
public class Singleton {
private static Singleton singleton;
public static Singleton getSingleton(){ if(singleton == null){ singleton = new Singleton(); } return singleton; }
public static void main(String[] args) { for (int i = 0; i < 10; i++) { Singleton singleton = Singleton.getSingleton(); System.out.println(singleton); } }}
输出结果
ps:看似没什么问题,可是如果在多线程环境下会出现什么问题呢?我们用代码看一下多线程环境下他是否还是单例呢?
public class Singleton {
private static Singleton singleton;
public static Singleton getSingleton(){ if(singleton == null){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } singleton = new Singleton(); } return singleton; }
public static void main(String[] args) { new Thread(() ->{ Singleton singleton = Singleton.getSingleton(); System.out.println(singleton); },"t1").start();
new Thread(() ->{ Singleton singleton = Singleton.getSingleton(); System.out.println(singleton); },"t2").start(); }}
根据输出结果显然此时获取出来的已经不再是单例,原因是什么呢?,当线程1进入getSingleton()方法时,首先判断是否为null,因为第一次肯定是null,而此时如果还没有执行singleton = new Singleton();,同时线程2进来发现不为null,那么此时就会创建两个不同的对象,那么就产生了两个不同的对象?
然后有人就说了这个很好解决啊,只需要在方法上加一个synchronized就可以保证线程安全了
public class Singleton {
private static Singleton singleton;
public static synchronized Singleton getSingleton(){ if(singleton == null){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } singleton = new Singleton(); } return singleton; }}
可是其他人就说你这样加是锁了整个方法,效率太低了,于是衍生出了这种加锁的方式。
public class Singleton {
private static Singleton singleton; public static Singleton getSingleton(){ if(singleton == null){ synchronized(Singleton.class){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } singleton = new Singleton(); } } return singleton; }}
可是这样就线程安全了吗?不是,如果线程1去获取实例,判断为null,拿到锁此时线程2也进来了,由于线程1还没有创建实例,此时判断也是为null,所以会继续执行,此时线程1即使拿了锁在创建对象,但是线程2已经走过了判断,获取出来的对象也不是单例的;于是DCL单例模式就诞生了,因为判断了两次所以称之为double-check-locking简称DCL单例。
public class Singleton {
private static Singleton singleton; public static Singleton getSingleton(){ if(singleton == null){ synchronized(Singleton.class){ if(singleton == null){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } singleton = new Singleton(); } } } return singleton; }}
那么问题又来了DCL单例确实没问题吗?确实没问题,但是需要注意的是private static Singleton singleton;这一句,是必须要加volatile,那为什么需要加volatile呢?如果不加会怎么样呢?我们先了解一下volatile的作用,volatile有两个作用
1.线程可见性,就是如果一个线程操作成员变量以后,计算机的问题并不是马上线程2就知道此时的值已经改变,他需要先告诉计算机的内存,然后线程2再从计算机内存哪儿知道已经改变,加了volatile以后就是直接告诉线程2我己经改变了值,省略中间告诉计算机内存哪一步。
2.禁止指令重排序,由于计算机的关系,计算机在为了效率考虑时如果认为前一步指令时间可能运行起来时间周期可能太长,会先执行比较快的指令例如赋值操作寄存器等,而后执行相对而言比较慢的指令。
3.单例为什么必须加volatile
先看一下,对象在初始化操作时,做了哪些操作呢?我们使用javap -c xxx.class来查看一个对象初始化时做了哪些汇编指令的操作。
public class MyObject { private static int a = 8;
public MyObject() { }
public static void main(String[] args) { MyObject myObject = new MyObject(); }
}
1: new这个指令是在创建对象时在堆内存开辟一个空间,同时给成员变量赋初始值上面为int类型那么就是0
2: dup复制这个对象
3: invokespecial执行构造方法,并赋值给成员变量,也就是这个时候才把8给a变量
4: astore_1建立关联,也就是把对象的地址给myObject5: return最后返回回到正题,那么如果不加volatile的话,就会发生指令重排序。
如果invokespecial 和astore_1发生指令重排序那么也就是先建立关联,然后再执行构造方法且赋值给成员变量,也就是说在DCL单例下,如果发生指令重排序,当第一个线程进来后判断为null然后调用new Singleton的时候此时的成员变量拿到的是个半初始状态的对象,刚好此时线程2进来因为已经建立了关联只是半初始状态,所以对象是有引用的,那么判断就不为null,然后直接去使用此单例,而此时使用就是一个半初始状态的单例所以会出现不可预估的问题,这就是为什么DCL单例要加volatile的原因。
- 十条有用的 Golang语言 技术
- Android 开发者必知必会的权限管理知识
- 你可能需要为你的 APP 适配 iOS 11
- Golang语言 redis 使用
- 页面结构化在 Android 上的尝试
- iOS 11: CORE ML—浅析
- 高性能网络编程7--tcp连接的内存使用
- Android 平台 Native 代码的崩溃捕获机制及实现
- go语言变参,匿名函数的多种用法
- 问题帖子--Concurrent Read/Write Map
- Android 混淆那些事儿
- H5 直播避坑指南
- H5 和移动端 WebView 缓存机制解析与实战
- 根据IE版本加载不同CSS样式的方法小结,解决低版本IE兼容问题
- 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 数组属性和方法
- CentOS7安装GUI界面及远程连接的实现
- Centos7.4环境安装lamp-php7.0教程
- iOS摄像头推流(2)
- ubuntu 16.04LTS 开机启动自动更换壁纸的实现方法
- linux中Centos7的LVM磁盘扩容问题
- 如何禁止网站内容被搜索引擎收录的几种方法讲解
- Apache由http自动跳转到https的多种方法
- CentOS服务器中安装FFmpeg的完整步骤
- linux中Centos7增加swap分区详解
- CentOS7如何重置root密码的方法
- Linux下RPM打包制作过程
- linux ssh端口转发的三种方式
- 学习Centos7软raid5的挂载
- linux中crw brw lrw等等文件属性是什么
- centos中文件与权限的基本操作教程