定义:
单例模式是GOF二十三中经典设计模式的简单常用的一种设计模式,单例模式的基本结构需满足以下要求。
- 单例模式的核心结构只有一个单例类,单例模式要保证这个类在运行期间只能被实例化一次,即只会被创建唯一的一个单例类的实例。
- 单例模式需要提供一个全局唯一能得到这个类实例的访问点,一般通过定义一个名称类似为GetInstance的公用方法实现这一目的。
实现:
恶汉式单例模式,当JVM加载类时就创建好实例对象。如果该实例一直没被使用,则浪费了系统资源。
public class MySingleTon_1 {
//在jvm加载类时就创建好对象
private static MySingleTon_1 uniqueSingleton = new MySingleTon_1() ;
//私有化构造函数
private MySingleTon_1(){};
public static MySingleTon_1 getInstanse(){
return uniqueSingleton;
}
}
懒汉式单例模式。当用户需要创造一个实例对象的时候才创建。
public class MySingleTon {
//利用一个静态变量来记录唯一的实例
private static MySingleTon uniqueSingleton ;
//私有化构造函数
private MySingleTon(){};
public static MySingleTon getInstanse(){
//如果实例对象为null,说明实例对象还没创建,则调用私有构造函数创建实例,并把他赋值给static对象。
if(uniqueSingleton == null){
uniqueSingleton = new MySingleTon();
}
return uniqueSingleton;
}
上面的单例模式在多线程下会出现问题,当多个线程在第一次调用getInstanse方法时,可能会创建多个实例对象。将上述方法 进行改进,在getInstanse上加上synchronized,或者在代码块上加上synchronized
public class MySingleTon_2 {
//利用一个静态变量来记录唯一的实例
private static MySingleTon_2 uniqueSingleton ;
//私有化构造函数
private MySingleTon_2(){};
//将此方法设置为同步的保证多线程下的安全性
public synchronized static MySingleTon_2 getInstanse(){
//如果实例对象为null,说明实例对象还没创建,则调用私有构造函数创建实例,并把他赋值给static对象。
if(uniqueSingleton == null){
uniqueSingleton = new MySingleTon_2();
}
return uniqueSingleton;
}
public class MySingleTon_2 {
//利用一个静态变量来记录唯一的实例
private static MySingleTon_2 uniqueSingleton ;
//私有化构造函数
private MySingleTon_2(){};
public static MySingleTon_2 getInstanse(){
//同步代码块
synchronized(MySingleTon_2.class) {
if (uniqueSingleton == null) {
uniqueSingleton = new MySingleTon_2();
}
}
return uniqueSingleton;
}
}
但是同步会对性能带来很大的问题,只有第一次执行方法时才需要同步,后续的执行并不需要同步。采用双重检查加锁的方法来进行同步。
public class MySingleTon_3{
//利用一个静态变量来记录唯一的实例 ,volatile 关键字保证uniqueSingleton赋予Mysingle_3实例时,多线程能正确的处理uniqueSingleton
private volatile static MySingleTon_3 uniqueSingleton ;
//私有化构造函数
private MySingleTon_3(){};
//
public static MySingleTon_3 getInstanse(){
//只有第一次会进入里面的判断
if(uniqueSingleton == null){
//进入同步代码块,再检查一次,如果没有实例则创建实例
synchronized (MySingleTon_3.class){
if(uniqueSingleton == null){
uniqueSingleton = new MySingleTon_3(); //1
}
}
}
return uniqueSingleton;
}
}
那么,如果上述的实现没有使用 volatile 修饰 ,会导致什么情形发生呢?
(1)、当我们写了 new 操作,JVM 到底会发生什么?
首先,我们要明白的是:new MysingleTon_3()是一个非原子操作。代码行uniqueSingleton = new MysingleTon_3(); 的执行过程可以形象地用如下3行伪代码来表示:
-
memory = allocate(); //1:分配对象的内存空间
-
ctorInstance(memory); //2:初始化对象
-
uniqueSingleton = memory; //3:使uniqueSingleton指向刚分配的内存地址
但实际上,这个过程可能发生无序写入(指令重排序),也就是说上面的3行指令可能会被重排序导致先执行第3行后执行第2行,也就是说其真实执行顺序可能是下面这种:
-
memory = allocate(); //1:分配对象的内存空间
-
uniqueSingleton = memory; //3:使uniqueSingleton指向刚分配的内存地址
-
ctorInstance(memory); //2:初始化对象
(2)、重排序情景再现
了解 new 操作是非原子的并且可能发生重排序这一事实后,我们回过头看使用 双重检查加锁 的同步延迟加载的实现:
我们需要重新考察上述//1处代码。此行代码创建了一个MysingleTon_3 对象并初始化变量 uniqueSingleton
来引用此对象。这行代码存在的问题是,在 Mysingle_3构造函数体执行之前,变量 uniqueSingleton
可能提前成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程将得到的是一个不完整(未初始化)的对象,会导致系统崩溃。下面是程序可能的一组执行步骤:
1、线程 1 进入 getInstance 方法;
2、由于 uniqueSingleton
为 null,线程 1 进入 synchronized 块;
3、同样由于 uniqueSingleton
为 null,线程 1 开始执行构造函数,但在构造函数执行之前,使实例成为非 null,并且该实例是未初始化的;
4、线程 1 被线程 2 预占;
5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 得到一个不完整(未初始化)的 MysingleTon_3 对象;
6、线程 2 被线程 1 预占。
7、线程 1 通过运行 uniqueSingleton
对象的构造函数来完成对该对象的初始化。
显然,一旦我们的程序在执行过程中发生了上述情形,就会造成灾难性的后果,而这种安全隐患正是由于指令重排序的问题所导致的。让人兴奋地是,volatile 关键字正好可以完美解决了这个问题。也就是说,我们只需使用volatile关键字修饰单例引用就可以避免上述灾难。
还有使用内部类来实现延时加载的单例模式,当内部类被加载时创建实例对象。
public class MySingleTon_4 {
// 私有内部类,按需加载,用时加载,也就是延迟加载
private static class Holder {
private static MySingleTon_4 singleton4 = new MySingleTon_4();
}
private MySingleTon_4(){
}
public static MySingleTon_4 getSingleton5() {
return Holder.singleton4;
}
}