(一)单例模式

2017-04-06  本文已影响0人  野狗子嗷嗷嗷

概念

确保某一个类只有一个实例,而且自行实例化,并向整个系统提供一个访问它的全局访问点,这个类称为单例类。

特性

单例类只能有一个实例
单例类必须自行创建自己的唯一的实例
单例类必须给所有其他对象提供这一实例

应用场景

优缺点

优点:

缺点:

代码实现

1. 懒汉式、线程不安全

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁synchronized,所以严格意义上它并不算单例模式。

与饿汉式的不同:不是一看到 instance 就初始化,饱汉要等到第一次使用的时候才初始化,不像饿汉一样一见到 instance 就初始化,这也被称为 懒加载,如果系统中很多这样的类,显然是懒加载的时候效率更高

public class Singleton {
    // 4:定义一个变量来存储创建好的类实例(关键点:声明单例对象是静态的)
    // 5:因为这个变量要在静态方法中使用,所以需要加上static修饰
    private static Singleton instance;
    // 1:私有化构造方法,好在内部控制创建实例的数目,限制产生多个对象(关键点:构造函数是私有的)
    private Singleton() {}
    // 2:定义一个方法来为客户端提供类实例
    // 3:这个方法需要定义成类方法,也就是要加static
    // 定义一个静态方法来为客户端提供类实例(全局访问点),这样就不需要先得到类实例
    public static Singleton getInstance() {
        // 6:判断存储实例的变量是否有值(关键点:判断单例对象是否已经被构造)
        if(instance == null) {
            // 6.1:如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
            instance = new Singleton();
        }
        // 6.2:如果有值,那就直接使用
        return instance;
    }
}

为什么这种实现是线程不安全的呢?如一个线程A执行到singleton = new Singleton();这里,但还没有获得对象(对象初始化是需要时间的),第二个线程B也在执行,执行到if(singleton == null)判断,那么线程B获得判断条件也是为真,于是继续运行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现两个对象,造成单例模式的失效!!

2. 懒汉式、线程安全

第一次调用才初始化,避免内存浪费。绝对线程安全,但是效率很低,99%情况下不需要同步。必须加锁 synchronized 才能保证单例,但加锁会影响效率,并发性能极差,事实上完全退化到了串行。

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    // 加锁 synchronized
    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

或者也可以这样:

public class Singleton {
    private static Singleton instance = null;   // 关键点 1:声明单例对象是静态的
    private Singleton() {}                      // 关键点 0:构造函数是私有的
    private static Object obj = new Object();
    public static Singleton GetInstance() {     // 通过静态方法来构造对象
        if (instance == null) {                 // 关键点 2:判断单例对象是否已经被构造
            lock(obj) {                         // 关键点 3:加线程锁
                instance = new Singleton();
            }
        }
        return instance;
    }
}

虽然这里判断了一次单例对象是否已经被构造,但是由于某些情况下,可能有延迟加载或者缓存的原因,只有关键点 2 这一次判断,仍然不能保证系统是否只创建了一个单例,也可能出现多个实例的情况。

3. 饿汉式

这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
饿汉单例模式线程安全。
值得注意的时,单线程环境下,饿汉与饱汉在性能上没什么差别;但多线程环境下,由于饱汉需要加锁,饿汉的性能反而更优。

public class Singleton {  
    // 定义一个静态变量来存储创建好的类实例,直接在这里创建类实例,只会创建一次
    private static Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {  
        return instance;  
    }  
}

也可以这样写(使用静态初始化块):

public class Singleton {
    private static Singleton instance = new Singleton();
    static {
        instance = new Singleton();
    }
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}

4. 双重检查锁(DCL,即 double-checked locking)

没有volatile修饰instance的双重检查锁版本仍然是线程不安全的,由于指令重排序,你可能会得到 “半个对象”。

如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理。
由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。

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

所以,在判断单例实例是否被构造时,需要检测两次,在线程锁之前判断一次,在线程锁之后判断一次,再去构造实例,这样就万无一失了。

或者也可以这样:

public class Singleton {
    private static Singleton instance = null;   // 关键点 1:声明单例对象是静态的
    private Singleton() {}                      // 关键点 0:构造函数是私有的
    private static Object obj = new Object();
    public static Singleton getInstance() {     // 通过静态方法来构造对象
        if (instance == null) {                 // 关键点 2:判断单例对象是否已经被构造
            lock(obj) {                         // 关键点 3:加线程锁
                if (instance == null) {         // 关键点 4:二次判断单例是否已经被构造
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这个版本看出优秀在哪里了吗?

缺点就是仍然可以通过反射等方式产生多个对象!

5. 静态内部类/Holder模式

我们既希望利用饿汉模式中静态变量的方便和线程安全;又希望通过懒加载规避资源浪费。Holder 模式满足了这两点要求:核心仍然是静态变量,足够方便和线程安全;通过静态的 Holder 类持有真正实例,间接实现了懒加载。

原理就是说,静态内部类会在第一次被使用的时候被初始化,并且也只会被初始化一次,所以也包含懒加载和线程安全的特性。

public class Singleton {  
    private Singleton() {}
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

StaticSingleton 被加载时,内部类不会被实例化,确保 StaticSingleton 类被载入 jvm 时,不会被初始化单例类,而当 getInstance() 方法被调用时,才加载 SingletonHolder,从而初始化 instance。同时用于实例的建立在类加载时完成,故天生对线程友好。

使用内部类完成单利模式,既可以做到延迟加载,也不用使用同步关键字,是一种比较完善的做法。

这种写法仍然使用 JVM 本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

6. 枚举单例

用枚举实现单例模式,相当好用,但可读性是不存在的。
枚举类型也是在第一次被使用的时候初始化,并且默认构造函数是 private 修饰,而且线程安全。
以上的单例还是在运用各种技巧来实现,最后一种简直是利用规则来实现。这种代码也及其简洁,只需要三行就可以实现,但是可惜的是在面试中并不适用,因为很多面试官可能也并不了解这个特性,那就是枚举类。Java 的枚举类是天生单例的,并且能够对多线程免疫,对序列化免疫,简直是神器。

// 将枚举的静态成员变量作为单例的实例
public enum Singleton {
    INSTANCE;
}

面试问题

单例模式实现的关键点

为何要检测两次?

有可能延迟加载或者缓存原因,造成构造多个实例,违反了单例的初衷。

构造函数能否公有化?

不行,单例类的构造函数必须私有化,单例类不能被实例化,单例实例只能静态调用

lock 住的对象为什么要是 object 对象,可以是 int 吗?

不行,锁住的必须是个引用类型。如果锁值类型,每个不同的线程在声明的时候值类型变量的地址都不一样,那么上个线程锁住的东西下个线程进来会认为根本没锁,相当于每次都锁了不同的门,没有任何卵用。而引用类型的变量地址是相同的,每个线程进来判断锁多想是否被锁的时候都是判断同一个地址,相当于是锁在通一扇门,起到了锁的作用。

如何选择各种实现方式

俗话说,No silver bullet,每一种实现都有其适用的场景。那么,我们如何选择单例的实现方式呢?答案是:取决于你所期望的内容。

如果你的单例类应用频繁,从系统启动后就需要使用,那么,饿汉式可能是一个不错的选择。类加载过程便已经完成了实例化的单例,在之后的调用过程中,无需再进行实例化,也无需害怕因为线程同步导致的性能损耗。

如果你的单例类占用较多资源,并且调用频率较低,那么或许 Double-Check 的懒汉式是一个不错的选择。在单例使用前,并不会被实例化,其所需要的资源也并不会被占用。

如果你的单例类属于某一个类库,或许 Double-Check 的懒汉式是一个不错的选择。一个功能丰富的类库中,并非所有的类都会被使用。然而 ClassLoader 的加载机制,并不一定会将其排除至外。所以,一个懒汉式的单例有可能降低类库使用者的资源损耗。

一般来说,如果项目中不需要针对多线程情况的话,懒汉式、饿汉式的写法都适用;如果需要保证多线程并行使用推荐静态内部类和枚举

懒汉式与恶汉式对比

懒汉式单例是典型的时间换空间,每次取值都要时间做判断,判断是否需要创建实例,当然如果没有外部取值就不会创建对象,节约内存空间。
饿汉式单例是典型的空间换时间,类装载时就初始化实例,不管有没有访问取值,不需要做判断节约时间,如果一直没有外部访问取值就浪费了内存空间。

你知道懒加载吗?是怎么用在单例创建上的?有什么优势?

如果某个实例的创建 (比如数据库连接池的创建) 需要消耗很多系统资源,就需要引入懒加载机制。即上面的代码在类加载时就创建好了,如果在程序中始终没用到这个实例就会浪费很多系统资源。
为避免这种情况,就引入了懒加载机制,即在使用这个实例的时候才创建它。

参考资料

设计模式干货系列:(四)单例模式【学习难度:★☆☆☆☆,使用频率:★★★★☆】
单例模式各版本的原理与实践
【创建型模式四】单例模式(Singleton)
单例模式(详解,面试问题)
如何正确地写出单例模式
单例模式 - 如何简单的理解单例模式
Java 设计模式学习(一) - 单例模式
如何写线程安全的单例模式
Java 设计模式之单例模式
设计模式(三)——JDK 中的那些单例

上一篇下一篇

猜你喜欢

热点阅读