设计模式之单例

设计模式之单例

定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

简单来说,也就是需要保证一个类在整个应用程序的生命周期内,只能存在一个实例(没有也行)。为了达成这个目标,应该要做到以下几点:

  1. 私有的构造器,如果是构造器被声明为public的,则无法控制其实例化;
  2. 一个获取实例的公开方法,也就是定义中所说的『提供一个访问它的全局访问点』;
  3. 一个用来保持此唯一实例的静态变量。

实现方法

首先,我们按照前面说的三点,写了以下代码:

public class Singleton {

    private Singleton instance;

    private Singleton(){}

    public Singleton GetInstance(){
        return instance;
    }
}

这样,显然是有问题的。

一是获取实例的公开方法getInstance,只是生命成public的,外部在没有Singleton的实例的情况下还是不能调用,就成了一个悖论了。所以需要把GetInstance方法声明为静态的。同样,由于需要被静态方法调用,同时还要用来保持唯一的实例,instance也需要声明为静态的。

二就是还缺少了对instance变量的初始化,即对构造器的调用。我们既可以再声明instance是就进行初始化,也可以在静态代码段中对它进行初始化。

于是乎,就有了下面两个版本的实现:

方法一:饿汉一

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton GetInstance(){
        return instance;
    }
}

方法二:饿汉二

public class Singleton {

    private static Singleton instance;
    
    static {
        instance = new Singleton();
    }

    private Singleton(){}

    public static Singleton GetInstance(){
        return instance;
    }
}

由于这两种方法没有实质上的区别,都是在类被加载的时候就进行了实例的初始化(所以被称为饿汉式)。这也是饿汉式的下的两个特点:

  1. 线程安全,因为在类加载时已经完成来实例化;
  2. 性能低,实例化后的对象在应用的声明周期中未必就会被使用,所以可能会产生计算的浪费。

方法三:懒汉

由于饿汉式在类加载时就完成了实例化,导致了可能存在性能浪费,所以我们就考虑看看能不能在类被使用时才被实例化呢。如果听说过『懒加载』这个的词话,应该就会觉得这很easy了,在之前的基础上,很轻松就写出了下面代码(懒汉式的单例)。

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {}

    public static LazySingleton GetInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

乍一看,已经是可以了,至少在单线程下已经没问题了。但是在多线程的场景下呢,很容易就会产生A、B两个线程同时调用GetInstance方法,A线程先判断instance为null,准备进行实例化,但在实例化之前B线程也进行了instance为null的判断,最终结果是两个线程分别调用了一次私有构造器进行实例化,第二次实例化的结果会将第一次的覆盖掉。

所以懒汉式有以下特点:

  1. 懒加载实现,第一次调用时实例化,不调用则不实例化,故计算效率高;
  2. 线程不安全,上面已经详细描述来是如何产生线程冲突的。

方法四:懒汉(线程安全)

为了解决上面方法的缺陷,也就是所谓的线程不安全,我们找到了synchronized这个关键字,也只加了这个关键字,得到下面的代码。

public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton() {}

    public static synchronized LazySingleton GetInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

与前一种方法相比,在实现上只是在getInstance方法上增加了synchronized关键字,使得GetInstance方法同步,同一时间只能有一个线程执行这个方法,那我们之前说的线程不安全的问题显然就不在了,这样是不是就完美来呢?世界没那么美好,我们又引入了新的问题。

由于是在整个GetInstance方法上加锁(同步),但是因为实际上只需要进行一次实例化(也只允许进行一次),所以绝大多数场景下是不需要同步的,所以在并发场景下会导致效率降低,相当于多车道在这里并成单车道了。

方法五:懒汉(双重检查锁定)

既然还有问题,那我们就来继续进行优化。上面的方法因为把整个GetInstance方法设置为synchronized,所以导致多线程在这里受阻,那我们把同步的范围缩小一点儿,看看情况会不会好一些。

public class LazySingleton {
    private static volatile LazySingleton instance;
    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized(LazySingleton.class){
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

优化之后,把锁定范围进行了收缩,只在需要进行初始化实例时才进行同步,之后就不再进行同步。

这样我们就得到了一种效率较高,并且线程安全的单例模式的构造方法。

PS. 如果有仔细看代码,您或许会发现我们在声明instance变量的时候,用了一个volatile关键字,如果需要一些解释的话,可以参考Java中的volatile在使用双层检查实现单例模式的解读,后面我也可能来单独说一下这个。

第六种:静态内部类

还有一种使用静态内部类来实现的单例,也被各种推荐。因为内部静态类是要在有引用了以后才会装载到内存的,这样就同样实现了懒加载;同时,静态内部类的静态变量的初始化,也是在被加载时进行的初始化,天然的完成来对进程安全的控制。

public class InnerStaticClassSingleton {
    private Singleton() {}

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static InnerStaticClassSingleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

To Be Continued

posted @ 2017-10-26 09:53  BigMan85  阅读(155)  评论(0编辑  收藏  举报