单例模式

1. 定义

单例模式:确保一个类只有一个实例,并提供一个全局访问点。

  • 按照定义来看,也可以设置一个全局变量,同样能实现要求,但是全局变量却存在问题,如果将对象赋值给一个全局变量,那么必须在程序一开始就创建好对象,万一这个对象非常耗费资源,而程序在这次的执行过程中又一直没有用到它,就形成浪费了。

2. 用处

有一些对象只需要一个,例如配置文件,工具类,线程池,缓存,日志对象等等。单例模式保证应用中有且只有一个实例。

常用的单例模式

(1)懒汉式:指全局的单例实例在第一次被使用时构建。
(2)饿汉式:指全局的单例实例在类装载时构建。

日常我们使用的较多的应该是懒汉式的单例,毕竟按需加载才能做到资源的最大化利用。

3. 实现

原理:利用静态类变量、静态方法和适当的访问修饰符

3.1. 经典的单例模式实现

/**
 * 经典的单件模式实现
 */
public class Singleton01 {

    // 利用一个静态变量来记录Singleton类的唯一实例
    private static Singleton01 uniqueInstance;

    /*
        别的成员变量
     */

    // 把构造器声明为私有的,只有从Singleton类内部才可以调用构造器
    private Singleton01() {
    }

    //
    public static Singleton01 getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton01();
        }
        return uniqueInstance;
    }

    /*
        别的方法实现
     */
}

上面是单例模式最经典也是最简单的实现方法,但是当程序使用多线程的时候就会出现问题,如下图所示:



在上图中的程序就会出现有多个对象的情况,这是因为使用了多线程,处理该多线程灾难的一个方法是把getInstance()变成同步(synchronized)方法。

3.2. 处理多线程

经典单例遇到多线程就会创造出多于一个的实例,只要把getInstance()变成同步(synchronized)方法,多线程灾难就可以轻易地解决了。

/**
 * 处理多线程:
 *      经典单例遇到多线程就会创造出多于一个的实例
 *      只要把getInstance()变成同步(synchronized)方法
 *      多线程灾难就可以轻易地解决了
 */
public class Singleton02 {

    // 利用一个静态变量来记录Singleton类的唯一实例
    private static Singleton02 uniqueInstance;

    /*
        别的成员变量
     */

    // 把构造器声明为私有的,只有从Singleton类内部才可以调用构造器
    private Singleton02() {
    }

    //
    public static synchronized Singleton02 getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton02();
        }
        return uniqueInstance;
    }

    /*
        别的方法实现
     */
}

但是这样问题又来了,为了解决多线程的问题,我们引入了同步(synchronized)方法,但是同步会降低性能,这又是另一个问题。

3.3. 改善多线程

经典单例遇到多线程会创造出多于一个的实例,但是使用同步(synchronized)方法又会降低性能,因此可以进一步改善多线程。

(1)如果getInstance()的性能对应用程序不是很关键,就什么都别做

(2)使用“饿汉式”创建实例,而不延迟实例化

这里就用到我们的“饿汉式”了,即,全局的单例实例在类装载时构建。
如果应用程序总是创建并使用单例对象的实例,或者在创建和运行时方面的负担不太繁重,就可以使用“饿汉式”创建实例。

package npu.yyl.pattern.singleton;

/**
 * 使用同步(synchronized)方法会降低性能
 * 如果应用程序总是创建并使用单例对象的实例,
 * 或者在创建和运行时方面的负担不太繁重,
 * 就可以使用“饿汉式”创建实例
 */
public class Singleton03 {

    // 在静态初始化器(static initializer)中创建单例
    // 这段代码保证了线程安全(thread safe)
    private static Singleton03 uniqueInstance = new Singleton03();

    private Singleton03() {
    }

    //
    public static Singleton03 getInstance() {
        // 已经有实例了,直接使用它
        return uniqueInstance;
    }

    /*
        别的方法实现
     */
}

利用这个做法,我们依赖JVM在加载这个类时马上创建此唯一的单例实例。JVM保证在任何线程访问uniqueInstance静态变量之前,一定先创建此实例。

(3)用“双重加锁”,在getInstance()中减少使用同步

利用双重检查加锁(double-checked locking),首先检查是否实例已经创建了,如果尚未创建,“才”进行同步。这样一来,只有第一次会同步,这正是我们想要的。

package npu.yyl.pattern.singleton;

/**
 * 利用双重检查加锁(double-checked locking),
 * 首先检查是否实例已经创建了,
 * 如果尚未创建,“才”进行同步。
 * 这样一来,只有第一次会同步。
 */
public class Singleton04 {

    // 利用一个静态变量来记录Singleton类的唯一实例
    private volatile static Singleton04 uniqueInstance;

    private Singleton04() {
    }

    public static synchronized Singleton04 getInstance() {

        if (uniqueInstance == null) {
            // 检查实例,如果不存在,就进入同步区块
            synchronized (Singleton04.class) {
                // 只有第一次才彻底执行这里的代码
                if (uniqueInstance == null) {
                    // 进入区块后,再检查一次。如果仍是null,才创建实例
                    uniqueInstance = new Singleton04();
                }
            }
        }
        return uniqueInstance;
    }

    /*
        别的方法实现
     */
}

volatile关键词确保,当uniqueInstance变量被初始化成Singleton实例时,多个线程正确的处理uniqueInstance变量。
这个方法关心了性能,大大减少了getInstance()的时间消耗。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容