单例模式
首先我们来考虑下,如何设计一个类,在系统中只能生成该类的一个实体?
懒汉,线程不安全
面对这个问题,我们可以想到把构造函数私有化,以禁止他人创建实例,我们可以写一个静态的实例,在需要的时候创建它。我们可以得到以下代码:
// version 1.0
public class Singleton {
private static Singleton singleton = null;
private Singleton() { }
public static Singleton getInstance() {
if (singleton== null) {
singleton= new Singleton();
}
return singleton;
}
}
私有(private)的构造函数,表明这个类是不可能形成实例了。这主要是怕这个类会有多个实例。
即然这个类是不可能形成实例,那么,我们需要一个静态的方式让其形成实例:getInstance()。注意这个方法是在new自己,因为其可以访问私有的构造函数,所以他是可以保证实例被创建出来的。
在getInstance()中,先做判断是否已形成实例,如果已形成则直接返回,否则创建实例。
所形成的实例保存在自己类中的私有成员中。
我们取实例时,只需要使用Singleton.getInstance()就行了。
懒汉,线程安全
上述代码在单线程下运行时没有问题的,但是放在多线程环境下就可能出问题。比如:当系统中不存在Singleton实例时,两个线程同时运行到判断 if (singleton== null) 时,那么两个线程都会通过判断并创建实例。就不符合单例模式的要求了。为了保护多线程环境下运行,我们需要加上同步锁,得到以下代码:
// version 2.0
public class Singleton
{
private static Singleton singleton = null;
private Singleton() { }
public static Singleton getInstance() {
//加上同步锁
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
return singleton;
}
}
这样的话,假如出现当系统中不存在Singleton实例时,两个线程同时运行到判断 if (singleton== null) 的情况时,两个线程想同时创建一个实例时,但由于在同一个时刻只有一个线程可以得到同步锁,当第一个线程加上锁时,第二个线程只有等待,第一个线程判断Singleton实例是否已经创建,发现没有实例,创建一个后释放锁,第二个线程加上同步锁,运行以上过程,发现实例已经被创建出来了,就不会重复创建了,这样可以保证我们在单线程环境中也只有一个实例。
但是version 2.0版本还是有点小问题,我们每次调用getInstance()获取实例时,都会视图加上一个同步锁,而加锁是非常耗时的一个操作,在美有必要时我们应该尽量避免。
双重校验锁
我们只是在没有创建实例前需要加锁,以保证只有一个实例,当实例创建后,已经不需要加锁了。所以我们可以改善代码:
// version 3.0
public class Singleton
{
private static Singleton singleton = null;
private Singleton() { }
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
}
这样只需要在实例没有创建时需要加锁操作,实例创建后,不需要加锁,只有在Singleton == null时才会加锁,其他时候不需要,所以version 3.0 的效率要高于 version 2.0。
双重校验锁+volatile
由于singleton = new Singleton()并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1.给 singleton 分配内存
2.调用 Singleton 的构造函数来初始化成员变量,形成实例
3.将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
对此,我们只需要把singleton声明成 volatile 就可以了。下面我们优化代码:
// version 4.0
public class Singleton
{
private volatile static Singleton singleton = null;
private Singleton() { }
public static Singleton getInstance() {
if (singleton== null) {
synchronized (Singleton.class) {
if (singleton== null) {
singleton= new Singleton();
}
}
}
return singleton;
}
}
使用 volatile 有两个功用:
1.这个变量不会在多个线程中存在复本,直接从内存读取。
2.这个关键字会禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。
但是,这个事情仅在Java 1.5版后有用,1.5版之前用这个变量也有问题,因为老版本的Java的内存模型是有缺陷的。
上面的代码比较复杂,我们能不能找一种更为优雅的方式?
饿汉
答案是可以的,我们可以在声明实例的时候就初始化,单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。
// version 5.0
public class Singleton
{
private volatile static Singleton singleton = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return singleton;
}
}
但是由于 static 和 final 变量,当这个类被加载的时候,new Singleton() 这句话就会被执行,就算是getInstance()没有被调用,类也被初始化了。
于是,这个可能会与我们想要的行为不一样,比如,我的类的构造函数中,有一些事可能需要依赖于别的类干的一些事(比如某个配置文件,初始化一些配置参数),我们希望他能在我第一次getInstance()时才被真正的创建。这样,我们可以控制真正的类创建的时刻,而不是把类的创建委托给了类装载器。
静态内部类(推荐)
对于上述我们不能控制类加载时机的问题,我们会想,如果什么时候调用 getInstance() 什么时候加载类就好了,因此我们可以优化代码:
// version 6.0
public class Singleton {
//创建一个私有静态内部类 SingletonHolder
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){ }
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
上面这种方式,仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它只有在getInstance()被调用时才会真正创建;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。
枚举(推荐)
在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了线程安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。
//version 7.0
public enum Singleton{
INSTANCE;
}
枚举法探究
很多人会对枚举法实现的单例模式很不理解。这里需要深入理解的是两个点:
- 枚举类实现其实省略了private类型的构造函数
- 枚举类的域(field)其实是相应的enum类型的一个实例对象
对于第一点实际上enum内部是如下代码:
public enum Singleton {
INSTANCE;
// 这里隐藏了一个空的私有构造方法
private Singleton () {
System.out.println("do something");
}
}
如果你这时候在另一个class中调用
public static void main(String[] args) {
System.out.println(Singleton.INSTANCE);
}
你可以看到:
do something
INSTANCE
对于一个标准的enum单例模式,最优秀的写法还是实现接口的形式:
// 定义单例模式中需要完成的代码逻辑
public interface MySingleton {
void doSomething();
}
public enum Singleton implements MySingleton {
INSTANCE {
@Override
public void doSomething() {
System.out.println("complete singleton");
}
};
public static MySingleton getInstance() {
return Singleton.INSTANCE;
}
}
comments powered by Disqus