为什么
单例模式属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
如何实现
实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
实现
懒汉式(线程不安全)
public class Singleton{
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
return new Singleton;
}
return singleton;
}
}
私有静态变量 singleton
被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 singleton
,从而节约资源。
这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (singleton == null)
,并且此时 singleton
为 null
,那么会有多个线程执行 singleton = new Singleton();
语句,这将导致实例化多次 singleton
。
懒汉式(线程安全)
public class Singleton{
private static Singleton singleton = null;
private Singleton(){}
public synchronized static Singleton getInstance(){
if(singleton == null){
return new Singleton;
}
return singleton;
}
}
只需要对 getInstance()
方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次singleton
。
但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 singleton
已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。
饿汉式(线程安全)
public class Singleton{
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
线程不安全问题主要是由于 singleton
被实例化多次,采取直接实例化 singleton
的方式就不会产生线程不安全问题。
但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。
区别
- 懒汉方式:指全局的单例实例在第一次被使用时构建。
- 饿汉方式:指全局的单例实例在类装载时构建。
静态内部类(线程安全)
public class Singleton{
private Singleton(){}
private static InnerClass{
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return InnerClass.singleton;
}
}
静态内部类的优点是:外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化singleton
,故而不占内存。即当Singleton
第一次被加载时,并不需要去加载InnerClass
,只有当getInstance()方法第一次被调用时,才会去初始化singleton
,第一次调用getInstance()
方法会导致虚拟机加载InnerClass
类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么,静态内部类又是如何实现线程安全的呢?首先,我们先了解下类的加载时机。
类加载时机:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。
- 遇到
new
、getstatic
、setstatic
或者invokestatic
这4个字节码指令时,对应的Java
代码场景为:new
一个关键字或者一个实例化对象时、读取或设置一个静态字段时(final
修饰、已在编译期把结果放入常量池的除外)、调用一个类的静态方法时。 - 使用
java.lang.reflect
包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。 - 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()
方法的类),虚拟机会先初始化这个类。 - 当使用JDK 1.7等动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这5种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是"有且仅有",那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。
我们再回头看下getInstance()
方法,调用的是InnerClass.singleton
,取的是InnerClass
里的singleton
对象,跟DCL
方法不同的是,getInstance()
方法并没有多次去new
对象,故不管多少个线程去调用getInstance()
方法,取的都是同一个singleton
对象,而不用去重新创建。当getInstance()
方法被调用时,InnerClass
才在Singleton
的运行时常量池里,把符号引用替换为直接引用,这时静态对象singleton
也真正被创建,然后再被getInstance()
方法返回出去,这点同饿汉模式。那么singleton
在创建过程中又是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:
虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕。如果在一个类的<clinit>()
方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()
方法后,其他线程唤醒之后不会再次进入<clinit>()
方法。同一个加载器下,一个类型只会初始化一次,在实际应用中,这种阻塞往往是很隐蔽的。
故而,可以看出singleton
在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。
那么,是不是可以说静态内部类单例就是最完美的单例模式了呢?其实不然,静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如Context
这种参数,所以,我们创建单例时,可以在静态内部类与DCL
模式里自己斟酌。
DoubleCheckLock(线程安全)
public class Singleton{
private volatile static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
return new Singleton;
}
}
}
return singleton;
}
}
singleton
只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 singleton
没有被实例化时,才需要进行加锁。
双重校验锁先判断 singleton
是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。
考虑下面的实现,也就是只使用了一个 if
语句。在 singleton == null
的情况下,如果两个线程都执行了if
语句,那么两个线程都会进入if
语句块内。虽然在 if
语句块内有加锁操作,但是两个线程都会执行 singleton = new Singleton();
这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if
语句:第一个 if
语句用来避免 singleton
已经被实例化之后的加锁操作,而第二个 if
语句进行了加锁,所以只能有一个线程进入,就不会出现 singleton == null
时两个线程同时进行实例化操作。
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
singleton
采用 volatile
关键字修饰也是很有必要的, singleton = new Singleton();
这段代码其实是分为三步执行:
- 为
singleton
分配内存空间 - 初始化
singleton
- 将
singleton
指向分配的内存地址
但是由于 JVM
具有指令重排的特性,执行顺序有可能变成 1>3>2
。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1
还没有执行2就执行了 1
和3
,此时T2
调用 getInstance()
后发现 singleton
不为空,因此返回 singleton
,但此时 singleton
还未被初始化。
使用 volatile
可以禁止 JVM
的指令重排,保证在多线程环境下也能正常运行。
枚举(线程安全)
在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了线程安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。
public enum Singleton {
INSTANCE;
}
枚举法探究
很多人会对枚举法实现的单例模式很不理解。这里需要深入理解的是两个点:
- 枚举类实现其实省略了
private
类型的构造函数 - 枚举类的域(field)其实是相应的enum类型的一个实例对象
对于第一点实际上enum内部是如下代码:
public enum Singleton {
INSTANCE;
// 这里隐藏了一个空的私有构造方法
private Singleton () {}
}
对于一个标准的enum单例模式,最优秀的写法还是实现接口的形式:
interface ISingleton{
void doSomething();
}
public enum Singleton implements ISingleton{
// 枚举
INSTANCE {
@Override
public void doSomething() {
System.out.println(getName() + " " + getAge());
}
};
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
private static Singleton getInstance(){
return Singleton.INSTANCE;
}
}