设计模式之单例模式

设计模式之单例模式

Posted by WangZiTao on Monday, November 12, 2018

单例模式

首先我们来考虑下,如何设计一个类,在系统中只能生成该类的一个实体?

懒汉,线程不安全

面对这个问题,我们可以想到把构造函数私有化,以禁止他人创建实例,我们可以写一个静态的实例,在需要的时候创建它。我们可以得到以下代码:

// 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;
    }
}
  1. 私有(private)的构造函数,表明这个类是不可能形成实例了。这主要是怕这个类会有多个实例。

  2. 即然这个类是不可能形成实例,那么,我们需要一个静态的方式让其形成实例:getInstance()。注意这个方法是在new自己,因为其可以访问私有的构造函数,所以他是可以保证实例被创建出来的。

  3. 在getInstance()中,先做判断是否已形成实例,如果已形成则直接返回,否则创建实例。

  4. 所形成的实例保存在自己类中的私有成员中。

  5. 我们取实例时,只需要使用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