1

在《Head First 设计模式》一书中,将单例模式称作单件模式。这里为了适应大环境,把它称之为大家更熟悉的单例模式。

一、了解单例模式

1.1 什么是单例模式

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

我们把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。想要获取单例实例,通过单例类是唯一的途径。单例类提供对这个实例的全局访问点:当你需要实例时,向类查询,它会返回单个实例。

1.2 单例模式 UML 图解

这里写图片描述

1.3 单例模式应用场景

  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。比如线程池、缓存、日志对象等。
  • 有状态的工具类对象。
  • 频繁访问数据库或文件的对象。
  • 以及要求只有一个对象的场景。

二、单例模式具体应用

2.1 经典的单例模式实现

采用经典单例模式实现代码有一个特点:如果我们不需要这个实例 (调用 getInstance() 方法),它就永远不会产生。因此这种方式也被称为“延迟实例化”(lazy instantiaze)。也被大家称为“懒汉式”。

单例类 Singleton

package com.jas.singleton;

public class Singleton {
    // 用静态变量来记录 Singleton 类的唯一实例
    private static Singleton uniqueInstance;

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

    /**
     * getInstance() 方法来实例化对象
     * 
     * @return Singleton 的实例对象
     */
    public static Singleton getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        
        return uniqueInstance;
    }
}

测试类

package com.jas.singleton;

public class SingletonTest {
    public static void main(String[] args) {
        
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        
        System.out.println(singleton1 == singleton2);
    }
}

     /**
     * 输出
     * true
     */

这虽然是经典的单例模式,但是这样做却存在着一个严重的问题:当多个线程同时访问 getInstance() 方法时,会产生线程安全问题,可能导致产生的实例可能会有多个,这样就违反了单例的原则。

2.2 处理多线程

存在线程安全问题,我们的第一反应可能是加同步锁。就像下面这样,这样做是可以解决线程安全问题,但是却降低了性能。因为只有在第一次执行该方法的时候,才真正需要同步。之后再调用此方法,同步反而会成为一种累赘。

    /**
     * 通过 synchronized 关键字,来保证不会同时有两个线程进入该方法
     * 
     * @return Singleton 的实例对象
     */
    public synchronized static Singleton getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        
        return uniqueInstance;
    }

2.3 改善多线程问题

为了符合大多数 Java 程序,很明显地,我们需要确保单例模式能在多线程的情况下正常工作。但是同步的做法会击垮其性能,所以提供以下几种方法来解决问题。

(1) 直接同步

直接同步虽然会降低性能,但是如果你的程序可以承受 getInstance() 造成的额外代价,同步确实是一种既简单又有效的方法。但是你必须知道,同步一个方法,可能会使程序的执行效率下降几十倍。因此,如果你需要频繁使用单例对象,那么你就要重新考虑设计了。

(2) “急切”创建实例

如果应用程序总是创建并使用单例创建的对象,或者在创建和运行时方面的负担不太严重,你可以急切 (early) 创建此对象。这种方式也被大家称为“恶汉式”。就像下面这样

package com.jas.singleton;

public class Singleton {
    //在静态初始化器中创建对象,用来保证线程安全
    private static Singleton uniqueInstance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return uniqueInstance;
    }
}

利用上面这种做法,我们依赖 JVM 在加载这个类时马上创建此唯一的实例。JVM 保证在任何时候任何线程访问 getInstance() 方法之前,一定会先创建此实例。这样一来就可以解决多线程之间的安全问题。

(3)双重检验加锁

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

package com.jas.singleton;

public class Singleton {
    //volatile 关键字用来保证内存可见性,使多线程正确处理 uniqueInstance 对象
    private static volatile Singleton uniqueInstance;

    private Singleton(){}

    public static Singleton getInstance(){
        //使用这种方式,只有第一次才会彻底访问并执这里的代码
        if(uniqueInstance == null){     //检查实例,如果不存在进入同步区
            synchronized (Singleton.class){
                if(uniqueInstance == null){     //进入同步区后,再检查一次。如果为 null,才开始创建实例
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

如果你性能是你关心的重点,那么这种方式会帮你大大减少访问 getInstance() 时的时间消耗。需要在注意的是:这种双重检验加锁的方式并不适用于 1.4 及之前更早的版本。

三、单例模式总结

3.1 优缺点总结

优点

  • 实例控制:单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
  • 灵活性:因为类控制了实例化过程,所以类可以灵活更改实例化过程。

缺点

  • 开销:虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例。
  • 可能的开发混淆:使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用 new 关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。
  • 对象生存期:不能解决删除单个对象的问题。在提供内存管理的语言,只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。

3.2 部分知识总结

  • 单例模式确保程序中一个类最多只有一个实例。单例模式也提供访问这个实例的全局点。
  • 如果你使用多个类加载器,可能导致单例模式失效,从而产生多个实例。
  • 确定性能和资源上的限制,我们应当选择合适的方案来实现单例模式。

参考资料


留兰香
123 声望5 粉丝