深入分析Java单例模式的各种方案
作者:秋? 发布时间:[ 2017/4/5 14:21:45 ] 推荐标签:测试开发技术 Java
单例模式
Java内存模型的抽象示意图:
所有单例模式都有一个共性,那是这个类没有自己的状态。也是说无论这个类有多少个实例,都是一样的;然后除此者外更重要的是,这个类如果有两个或两个以上的实例的话程序会产生错误。
非线程安全的模式
public class Singleton {
private static Singleton instance;
private Singleton(){
}
public static Singleton getInstance() {
if (instance == null) //1:A线程执行
instance = new Singleton(); //2:B线程执行
return instance;
}
}
普通加锁
public class SafeLazyInitialization {
private static Singleton instance;
public synchronized static Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
}
出于性能考虑,采用双重检查加锁的模式
双重检查加锁模式
public class Singleton{
private static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(null == singleton){ //第一次检查
synchronized(Singleton.class){ //加锁
if(null == singleton){ //第二次检查
singleton = new Singleton();//问题的根源出在这里
}
}
}
return singleton;
}
}
双重检查加锁模式相对于普通的单例和加锁模式而言,从性能和线程安全上来说都有很大的提升和保障。然而双重检查加锁模式也存在一些隐蔽不易被发现的问题。首先我们要明白在JVM创建新的对象时,主要要经过三个步骤。
· 分配内存
· 初始化构造器
· 将对象指向分配的内存地址
这样的顺序在双重加锁模式下是么有问题的,对象在初始化完成之后再把内存地址指向对象。
问题的根源
但是现代的JVM为了追求执行效率会针对字节码(编译器级别)以及指令和内存系统重排序(处理器重排序)进行调优,这样的话有可能(注意是有可能)导致2和3的顺序是相反的,一旦出现这样的情况问题来了。
java源代码到终实际执行的指令序列:
前面的双重检查锁定示例代码的(instance = new Singleton();)创建一个对象。这一行代码可以分解为如下的三行伪代码:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
上面三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的,详情见参考文献1的“Out-of-order writes”部分)。2和3之间重排序之后的执行时序如下:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
ctorInstance(memory); //2:初始化对象
多线程并发执行的时候的情况:
解决方案
基于Volatile的解决方案
先来说说Volatile这个关键字的含义:
· 可以很好地解决可见性问题
· 但不能确保原子性问题(通过 synchronized 进行解决)
· 禁止指令的重排序(单例主要用到此JVM规范)
Volatile 双重检查加锁模式
public class Singleton{
private volatile static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(null == singleton){
synchronized(Singleton.class){
if(null == singleton){
singleton = new Singleton();
}
}
}
return singleton;
}
}
基于类初始化的解决方案
利用静态内部类的方式来创建,因为静态属性由JVM确保第一次初始化时创建,因此也不用担心并发的问题出现。当初始化进行到一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以singleton仍然是单例的。
这个方案的实质是:允许“问题的根源”的三行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11