单例模式介绍
一个类只允许创建一个实例的设计模式被称为单例模式
当一个类被反复实例化并重复使用时,为了节约内存空间,会用到这种模式
单例模式分为饿汉式和懒汉式,下面一一介绍
饿汉式
所谓的饿汉式,顾名思义,很饿就迫不及待地想创建实例对象,而java中最早可以创建的时候是类加载的时候,类加载首先会初始化静态代码,那么饿汉式就应该这样写:
public class Singleton {
// 类加载初始化静态代码的时候就创建实例
private static Singleton instance = new Singleton();
// 不让外界new对象,所以构造器私有化
private Singleton(){}
// 开放一个接口来让外界拿到唯一实例化的对象,加static是由于不能实例化对象,只能通过静态函数Singleton.getInstance()来获取
public static Singleton getInstance(){return instance;}
}
懒汉式
懒汉,就是很懒,在用户需要的时候我再创建对象,初始版本v1版就这样写
public class Singleton {
// 懒人模式,先不实例化,需要的时候才实例化,懒人不到最后一刻不想行动
private static Singleton instance;
// 不让外界new对象,所以构造器私有化
private Singleton(){}
// 开放一个接口来让外界拿到唯一实例化的对象
public static Singleton getInstance(){
// 调用方法时才实例化对象
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
这种方法存在一个问题,那就是在多线程环境下会创建两个实例。
比如当实例还没有被从初始化时,两个线程A和B,A与B同时进入执行Singleton.getInstance()函数,执行if(instance == null)
,同时判断为真,导致初始化了两个实例对象,与单例模式产生矛盾。
根据多线程知识,我们可以加锁来解决这个问题,v2版来了:
public class Singleton {
// 懒人模式,先不实例化,需要的时候才实例化,懒人不到最后一刻不想行动
private static Singleton instance;
// 不让外界new对象,所以构造器私有化
private Singleton(){}
// 开放一个接口来让外界拿到唯一实例化的对象
public static Singleton getInstance(){
// 加锁,防止多线程安全问题
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
return instance;
}
}
加锁是解决了多线程安全问题,解决的是在没有创建对象的时候,防止多线程创建多个实例对象的问题。但是上面这样写会带来一个效率问题,就是每次执行Singleton.getInstance()函数时,都会加锁,如果我之前就创建好了对象,那其实就不用加锁了,所以上面这样写会带来效率问题,那么在加锁的外层再加一个判断空不就行了,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)
public class Singleton {
// 先不实例化
private static Singleton instance;
// 不让外界new对象,所以构造器私有化
private Singleton(){}
// 开放一个接口来让外界拿到唯一实例化的对象
public static Singleton getInstance(){
// 多加一层判断,如果对象已经创建好了,就不用加锁,直接返回就行了
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
可能会有疑问,那就是既然外面判断instance是否为空的if,那就说明instance不为空?里面的if是不是可以去掉?
是不行的,因为这种想法只是考虑到单线程的情况,考虑多线程,假如进入外面的if之后,别的线程在这个时候创建了实例,那么instance就不为空了,里面的if就不成立了
就此我们解决了多线程安全+效率问题。但是还是有不省心的地方,那就是java的指令重排带来的问题
指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
其中2和3的顺序是可以调换的,也就是可以执行1->3->2,当线程A执行完1,3时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。
使用volatile关键字可以防止指令重排序。使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生异常了。
volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
public class Singleton {
// 给instance对象加上volatile,防止指令重排
private volatile static Singleton instance;
// 不让外界new对象,所以构造器私有化
private Singleton(){}
// 开放一个接口来让外界拿到唯一实例化的对象
public static Singleton getInstance(){
// 多加一层判断,如果对象已经创建好了,就不用加锁,直接返回就行了
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
到此,单例模式基本上已讲完了,下面是一点拓展,增加知识面,让面试更有谈资,自行取之:
懒汉式拓展
还有一种巧妙的方法,就是使用静态内部类实现单例模式:
public class Singleton {
// 建立一个静态内部类来实现单例模式
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
// 当外界使用Singleton.getInstance()时,LazyHolder.INSTANCE静态内部类才会初始化,创建实例对象
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
静态内部类是私有的,外界不能调用,当外界第一次调用getInstance()方法时,return LazyHolder.INSTANCE;
会初始化静态内部类仅一次,创建唯一的实例对象,并且静态内部类也可以避免线程安全问题
使用静态内部类和上面的双重验证都不能避免一个问题,那就是通过反射强行拿到对象
学习过反射知识的同学,应该还能够回忆起来,可以通过反射强行获取一个类的对象,即便你的构造器是私有的,来回忆回忆:
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//强行设置私有构造器为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象,输出false
System.out.println(singleton1.equals(singleton2));
这样就打破了单例模式
解决方案就是使用枚举类型
public enum SingletonEnum {
INSTANCE;
}
额,一行代码就解决了,java的enum语法糖会阻止反射强行获取
在《effective java》中说道,最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。