意图
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
实现(饿汉、懒汉、双重校验锁、静态内部类)
饿汉单例模式
public class Singleton {
private static final Singleton instance=new Singleton();//加载类时,自动初始化实例
private Singleton(){}//私有构造器
public static Singleton getInstance(){//静态工厂方法
instance;
}
//其他方法
}
在这个类被加载时,静态变量instance会被初始化,单例类的唯一实例就被创建出来。
这种方式基于类加载的机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定是否有其他的方式导致类装载(例如调用其他的静态方法),如果不是在调用getInstance()方法的时候初始化instance,显然没有达到lazy loading的效果。
懒汉单例模式(非线程安全)
public class Singleton {
private static Singleton instance;
private Singleton(){}//私有构造器
public static Singleton getInstance(){
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。
例如,如果A、B两个线程各自创建了一个实例,显然这违背了单例实例的初衷。
懒汉单例模式(线程安全)
public class Singleton {
private static Singleton instance;
private Singleton(){}//私有构造器
public synchronized static Singleton getInstance(){//在方法上进行同步
if(instance==null){
instance=new Singleton();
}
return instance;
}
}
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,因为只有第一次创建实例时需要同步,其余时候不需要同步,但是我们却在每次获取单例时都进行了同步。
静态内部类单例模式
public class Singleton{
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}//私有构造器
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//其他方法
}
这种方式同样利用了类加载的机制来保证线程安全,它跟饿汉单例模式不同点在于(很细微的差别):饿汉单例模式只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方(其他静态方法)被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉单例模式就显得很合理。
双重校验锁(非线程安全)
public class Singleton{
private static Singleton instance;
private Singleton(){}//私有构造器
public static Singleton getInstance(){
if(singleton==null){//第一次校验
synchronized(Singleton.class){//同步
if(singleton==null)//第二次校验
singleton=new Singleton();
}
}
return singleton;
}
}
“双重校验锁”将竞争锁的时机限定在第一次创建实例时,提高了并发性能。但是却存在潜在的线程安全问题。
由于new关键字代表的实例化对象的创建操作,实际上并非是一个原子性操作,而是分为:分配内存空间、初始化对象、引用赋值三个操作。伪代码:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
因为虚拟机的指令重排序,可能出现如下执行过程。
线程B访问了未进行初始化的实例,可能会出现非预期的结果。
双重校验锁(线程安全)
我们可以利用volatile关键字禁止指令重排序。
public class Singleton {
private static volatile Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(singleton==null){
synchronized(Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}