单例模式(Singleton)的几种实现
单例模式
是为了确保类只能生成一个对象,通常是该类需要消耗较多的资源或在逻辑上没有多个实例的情况。一般需要将构造函数私有化,使得用户无法手动new出对象,还需要向外暴露一个公有的静态方法以便获取单例对象。
本篇文章主要总结单例模式在Java语言中的实现方法。
1、 饿汉模式
顾名思义,饿汉嘛,经不起等待,也就是在使用之前就已经初始化好了单例对象。
public class Singleton {
//静态成员变量在类加载时就已经初始化。
private static Singleton sInstance = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return sInstance;
}
}
这样效率比较低,这个实例很有可能在之后程序运行时并未被使用,就浪费了系统资源。
2、 懒汉模式
懒汉就会把事情推迟到最后一刻,也就是在外部调用getIntsance静态方法获取单例对象时再初始化。
相应的,在getIntsance方法中需要进行判断,若之前单例对象已经new出来了就直接返回即可。
public class Singleton {
private static Singleton sInstance;
private Singleton(){}
public static Singleton getInstance(){
if(sInstance == null){
sInstance = new Singleton();
}
return sInstance;
}
}
这种实现线程不安全。若有多个进程同时进入了if判断,每个线程都会new一个对象,就不能叫做单例了。
可以通过在方法上加锁来解决。
public class Singleton {
private static Singleton sInstance;
private Singleton(){}
public synchronized static Singleton getInstance(){
if(sInstance == null){
sInstance = new Singleton();
}
return sInstance;
}
}
但是加了锁之后效率就降低了,只能有一个线程进入到getInstance方法中。我们要想清楚真正应该锁住的是“判断+new”的操作,因为一旦对象new出来就可以直接返回了,也不会存在冲突情况。
我们可以在上一版实现的基础上再次改进,只让synchronized 锁住一部分代码块来提高效率。
public class Singleton {
private static Singleton sInstance ;
private Singleton(){}
public static Singleton getInstance(){
synchronized(Singleton.class){
if(sInstance == null){
sInstance = new Singleton();
}
}
return sInstance;
}
}
那这个版本是不是就完美了呢?当然不是!
3、DCL(Double Check Lock)
还是我们在上面提到的——“一旦对象new出来就可以直接返回了,也不会存在冲突情况。”,在这种情况下线程根本没必要进入到锁住的代码块,所以我们还需要再加一个外层的判断。
public class Singleton {
private static Singleton sInstance;
private Singleton(){}
public static Singleton getInstance(){
if(sInstance == null){
synchronized(Singleton.class){
if(sInstance == null){
sInstance = new Singleton();
}
}
}
return sInstance;
}
}
其实这一种实现就是DCL了,第一层的check是为了避免不必要的同步,第二层的check是为了在null的情况下创建实例。
但这一版本还是有点瑕疵,因为指令重排
的存在。
简单来说,我们写的JAVA代码在底层都是一条条的指令,可以实现一些操作,比如说赋值等。JVM为了提高程序执行的效率,会按照一定的规则允许进行指令优化,可能不会按照我们写代码时的顺序严格执行。
sInstance = new Singleton();
这行代码最终编译成的汇编指令主要做了三件事:
- 给Singleton的实例分配内存;
- 调用Singleton的构造函数进行成员的初始化;
- 将sInstance 对象指向分配的内存空间。(sInstance 此时不为null)
由于指令重排的存在,执行顺序可能并非我们预想的1-2-3,有可能是1-3-2。在后者这种情况下,执行完3之后尚未执行2时,线程进行了切换,在其他线程中sInstance 就非空了,但实际上成员并未进行初始化,在之后的使用中可能会出问题,这就是DCL失效问题。
为了避免这种情况发生,我们在声明实例时加上一个volatile关键字可以禁止指令重排。
private
volatile
static Singleton sInstance;
4、静态内部类
DCL的写法稍有不慎就出错。静态内部类实现单例就更为简便。
public class Singleton {
private Singleton(){}
public static Singleton getInstance(){
return InnerInstance.sInstance;
}
private static class InnerInstance{
private static final Singleton sInstance = new Singleton();
}
}
只有在第一次调用Singleton 的getInstance方法时才会导致sInstance初始化。这种方法是被推荐使用的。
5、枚举
枚举在JAVA中和普通的类是一样的,可以拥有字段和方法。枚举市里的创建默认是线程安全的,在任何情况下都是一个单例。
public enum Singleton {
INSTANCE;
public void myMethod(){
System.out.println("Hello");
}
}
以上五种是比较常见的单例实现方式,但前四种在一个特殊情况下会重新创建新对象的情况——反序列化。
即时我们已经将构造函数私有化了,反序列化时依然可以通过特殊途径去创建一个新的实例,相当于调用了该类的构造函数。
为了杜绝在反序列化时重新创建对象,需要加入以下方法:
private Object readResolve() throws ObjectStreamException{
return sInstance;
}