单例模式,顾名思义,指的是一个类只存在一个实例。
那么,如何保证某一个类只存在一个实例呢?对象的创建是通过类的构造函数来实现的(也可以通过clone方式,但是这种方法前提是首先有一个对象;或者通过反序列化,这种方法同样也需要原本类的对象存在),所以就需要保证类的构造函数只能被调用一次。若是将构造函数的权限设置为public的,这样显然无法满足要求。所以需要将构造方法隐藏起来,而且构造方法只在类还未创建实例的情况下被调用,如果类已经创建了实例,则不再调用。
所以可以将构造函数的访问权限设置为最私密的private,同时暴露一个公共的接口给调用者,而在接口处统一的返回实例给调用者。如此,对于每一个需要对象的调用者,可以通过这个接口保证返回的都是同一个实例。
这样的话,类就需要持有一个自身类的实例,当需要调用者调用公共的接口想要获得这个类的实例的时候,统一的将这个实例返回。
public class Singleton{
private static Singleton Instance=new Singleton();//持有一个类的实例
private Singleton(){};//私有的构造方法
public static Singleton getInstance(){
return Singleton.instance;
}
}
这种方式实现的单例模式也叫做饿汉式,实例的创建时机是在类加载的时候静态变量初始化的时候。至于为什么叫做饿汉式,可能是因为这种方式是在类加载的时候获得唯一实例的,而不考虑类加载时是否需要这个类的实例(可能只是初次调用这个类的其他类方法就会进行唯一的创建)。
与这种方法不同的另一种实现叫做懒汉式,可能是因为懒汉是要到事情迫在眉睫的才会去做,而懒汉式的单例模式实现这种方式是在需要这个类的实例再去创建这个实例,是一种延迟创建对象的实现方式。
那么,如何实现这种呢?即不是让类加载的时候就进行对象创建,而是等到真正需要这个对象的时候(也就是调用公共的接口想要获得实例的时候),可以想到的是,将创建对象的过程放到接口内,这样就可以保证创建对象的时机是在调用这个公共接口的时候。但是显然仅仅这样做,是没有办法保证实例是唯一的,每一次的调用都将会创建一个新的实例返回。
可以想到的一种办法,在每次调用公共接口的时候,检查是否这个类的实例是否已经创建,这样就需要一个标识,来标识类的实例是否已经被创建,如果被创建了,就直接返回这个类的实例,如果没有创建,就先创建,再返回。所以这种方式,类还是需要持有自身的实例,由于类的实例持有是通过保存这个对象的引用来实现的,所以可以通过判断这个引用是否为null,就可以判断实例是否已经被创建。
public class Singleton{
private static Singleton Instance=null;
private Singleton(){};//私有的构造方法
public static Singleton getInstance(){
if(instance==null)
instance=new Singleton();
return Singleton.instance;
}
}
这种就是懒汉式的设计单例模式了,这种方式虽然是实现了延迟创建对象的功能(由于对象的创建是需要相当程度的占用资源,如果大量的对象创建都集中在类加载时,这样是非常不好的),但是这种方式只能保证在单线程下是安全的,也就是在多线程下是有问题的。
原因是因为,由于对象的创建不具有原子性,对象的创建可以简单的分为几个阶段1、在堆中开辟可用的空间 2、对象进行初始化 3、将对象的引用复制给引用变量,也就是“=”这个操作。java虚拟机jvm会对指令进行重排序来优化程序,jvm会保证单线程下重排序不会对结果产生影响,假设重排序可能是先把对象的引用传给引用变量,然后再进行初始化对象,就算是这样,只要保证在调用对象之前,已经完成了初始化,那么重排序将不会影响结果。但是在多线程的情况之下,这种重排序的机制将可能出现问题,假设线程1为第一个调用new Singleton的线程,而且在它创建对象的时候发生了重排序,jvm在未将对象初始化的引用先传递给了引用变量instance,而此时,线程2也在访问getInstance(),那么当它进行instance==null判断的时候,将会认为对象已经创建(实际上对象还未初始化)而直接返回,如果此时再继续进行对象的调用,将会发生错误,因为对象没有初始化完成。而且多个线程最大权限访问同一个资源,也是会出问题的。(线程1进行对象初始化后还未进行引用赋值,线程2此时将判断对象还未创建,从而进入if子句中进行对象创建,这样就无法保证实例唯一)
那么如何解决这个问题呢?首先想到的可能是加锁,既然在对象创建的未完成的时候可能有其他的线程访问这个资源,那么可以将这个getInstance()加上synchronized关键字,每次只允许一个线程访问这个方法,这样确实杜绝了上面所说的问题,但是由于在这么大的粒度下面加上同步,将会非常影响效率。
public class Singleton{
private static Singleton Instance=null;
private Singleton(){};//私有的构造方法
public static synchronized Singleton getInstance(){
if(instance==null)
instance=new Singleton();
return Singleton.instance;
}
}
因为这种方法效率非常差,所以还需要另外想办法解决多线程下单例模式的问题。所以有人想到一种将同步的粒度缩小的办法,叫做双重校验锁。
public class Singleton{
private static Singleton Instance=null;
private Singleton(){};//私有的构造方法
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return Singleton.instance;
}
}
这种方法解决了上一个方法效率的问题,也解决了只创建唯一的实例,但是由于重排序机制,将可能会导致双重校验锁失效,想到了既然问题是重排序机制下其他线程访问了未初始化完成的对象造成的可能错误。那么可以想办法让jvm不进行重排序,volatile关键字有两种语义,一个是线程间的可见性,保证其标识的值是最新的值,从内存中读取,而不是缓存的值,另一种语义是禁止重排序。所以可以用volatile解决双重校验锁失效的问题。
public class Singleton{
private volatile static Singleton Instance=null;
private Singleton(){};//私有的构造方法
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return Singleton.instance;
}
}
还有一种方法是静态内部类的方法,饿汉式的方式没有多线程下的诸多问题,但是由于其不具有延迟性,而不被采用,但是可以将这种类加载时由类直接创建实例的方式的优点利用,通过静态内部类的方式,这样既保证了延迟创建对象,又多线程安全。
public class Singleton{
private static class SingletonHolder{
private static Singleton instance=new Singleton();
}
private Singleton(){};
public static Singleton getInstance(){
return SingletonHoder.instance;
}
}
上面的所有的类内的单例对象引用也可以定义为final。
还有一种是枚举的方式,由于枚举类型的枚举值的对象创建是由java自己实现的,它的构造器是私有的。
public enum Singleton{
INSTANCE;//枚举值,java会在创建class的时候将它定义为static final的,它的值是Singleton类型的对象
同样的,和一般类一样,后面还可以按照需要,定义类和实例的属性或者方法。这些都是可以的。
}
Singleton.INSTANCE就是singleton类型的唯一实例了。