详细内容请移步至语雀文档:点击跳转
一、单例模式概述
1.1 什么是单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如J2EE 标准中的 ServletContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,即保证一个类仅有一个实例。这个类提供了一种访问其唯一对象的全局访问点,可直接访问,不需要实例化该类的对象。
单例模式解决了一个全局使用的类频繁的创建与销毁。单例模式提供一个获取自己实例对象的方法,其构造方法和成员变量均为私有。
单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。
1.2 单例模式的特点
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
1.3 单例模式的优缺点
1.3.1 单例模式的优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
1.3.2 单例模式的缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
1.4 单例的应用场景
对于Java来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
- 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- 频繁访问数据库或文件的对象。
- 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
二、单例模式示例
①创建一个单例类
public class SingleObject {
//在该单例类中自己创建自己的实例对象
private static SingleObject instance = new SingleObject();
//构造函数为private修饰,该类不会被实例化
private SingleObject() {
}
//获取唯一的可用对象
public static SingleObject getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World");
}
}
②测试
public class SingletonPatternDemo {
public static void main(String[] args) {
//直接用构造函数创建对象时,编译出错,因为构造函数singleObject不可见
//SingleObject singleObject = new SingleObject();
//获取为一可用的实例
SingleObject instance = SingleObject.getInstance();
instance.showMessage();
}
}
三、单例模式的六种实现方式
单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三种。
3.1 懒汉式单例
3.1.1 懒汉式(线程不安全)
是否Lazy初始化(懒加载) | 是 |
是否多线程安全 | 否 |
实现难度 | 易 |
描述:这种方式是最基本的实现方式,也是最简单的单例模式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式lazy loading(懒加载)很明显,不要求线程安全,在多线程不能正常工作。在该实现方式中,构造方法是私有的,别人不能访问你的实例,提供一个static方法以供他人获取你的实例对象。
public class Singleton {
private static Singleton instance;
//构造方法私有
private Singleton (){}
//供他人获取实例对象的静态方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3.1.2 懒汉式(线程安全)
是否Lazy初始化(懒加载) | 是 |
是否多线程安全 | 是 |
实现难度 | 易 |
描述:这种方式具备很好的懒加载,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
在第一种懒汉式中加入了一个关键字synchronized, 使用synchronized保证线程同步,保证同时只有一个进程进入此方法。从而保证并发安全。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
3.2 饿汉式
是否Lazy初始化(懒加载) | 否 |
是否多线程安全 | 是 |
实现难度 | 易 |
描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时该单例对象就被初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达懒加载的效果。
//静态常量
public class Singleton {
//类加载时就获得实例对象
private static final Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
//静态代码块
public class Singleton {
//类加载时就获得实例对象
private static final Singleton instance ;
static{
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
3.3 双检锁/双重校验锁(DCL,即 double-checked locking)
是否Lazy初始化(懒加载) | 是 |
是否多线程安全 | 是 |
实现难度 | 复杂 |
描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。方法声明上去除了synchronized关键字,多线程进入方法内部,判断是否为null,如果为null,多个线程同时进入if块内,此时,我们是用Singleton Class对象同步一段方法。保证只有一个线程进入该方法。并且判断是否为null,如果为null,就进行初始化。我们想象一下,如果第一个线程进入进入同步块,发现该实例为null,于是进入if块实例化,第二个线程进入同步内则发现实例已经不是null,直接就返回了,从而保证了并发安全。那么这个和第二种方式又什么区别呢?第二种方式的缺陷是:每个线程每次进入该方法都需要被同步,成本巨大。而第四种方式呢?每个线程最多只有在第一次的时候才会进入同步块,也就是说,只要实例被初始化了,那么之后进入该方法的线程就不必进入同步块了。就解决并发下线程安全和性能的平衡。虽然第一次还是会被阻塞。但相比较于第二种,已经好多了。
修饰变量的volatile关键字,为什么要用volatile关键字呢?这是个有趣的问题。我们好好分析一下:首先我们看,Java虚拟机初始化一个对象都干了些什么?总的来说,3件事情:
1.在堆空间分配内存
2.执行构造方法进行初始化
3.将对象指向内存中分配的内存空间,也就是地址
但是由于当我们编译的时候,编译器在生成汇编代码的时候会对流程进行优化,优化的结果式有可能是123顺序执行,也有可能式132执行,但是,如果是按照132的顺序执行,走到第三步(还没到第二步)的时候,这时突然另一个线程来访问,走到if(singleton == null)块,会发现singleton已经不是null了,就直接返回了,但是此时对象还没有完成初始化,如果另一个线程对实例的某些需要初始化的参数进行操作,就有可能报错。使用volatile关键字,能够告诉编译器不要对代码进行重排序的优化。就不会出现这种问题了。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
//多线程直接访问,不做控制,不影响性能
if (singleton == null) {
//如果有多个线程进入,则进入同步块,其余线程等待
synchronized (Singleton.class) {
//此时第一个线程判断为空,第二个进来时就不为空
if (singleton == null) {
//第一个线程实例化此对象
singleton = new Singleton();
}
}
}
return singleton;
}
}
3.4 登记式/静态内部类
是否Lazy初始化(懒加载) | 是 |
是否多线程安全 | 是 |
实现难度 | 一般 |
描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3.5 反射和反序列化破坏单例
我们知道Java的反射几乎是什么事情都能做,管你什么私有的公有的。都能破坏。我们是没有还手之力的。精心编写的代码就被破坏了,而反序列化也很厉害,但是稍微还有点办法遏制。什么办法呢?重写readResolve方法。
我们看到:我们重写了readResolve方法,在该方法中直接返回了我们的内部类实例。重写readResolve() 方法,防止反序列化破坏单例机制,这是因为:反序列化的机制在反序列化的时候,会判断如果实现了serializable或者externalizable接口的类中包含readResolve方法的话,会直接调用readResolve方法来获取实例。这样我们就制止了反序列化破坏我们的单例模式。
3.6 枚举
是否Lazy初始化(懒加载) | 否 |
是否多线程安全 | 是 |
实现难度 | 易 |
描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}