likes
comments
collection

Java 设计模式 | 单例模式

作者站长头像
站长
· 阅读数 15

概述

单例模式,是设计模式中最常见的模式之一,它是一种创建对象模式,用于产生一个对象的具体实例,可以确保系统中一个类只会产生一个实例。

优缺点

优点

  1. 对于频繁使用的对象,可以省去 new 操作花费的时间,尤其对那些重量级对象而言,削减了一笔非常客观的系统开销。
  2. 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,从而减轻 GC 压力,缩短 GC 停顿时间。

缺点

  1. 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  2. 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  3. 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

场景

  1. Spring 中创建的 Bean 实例默认都是单例。
  2. 数据库连接池的设计与实现。
  3. 多线程的线程池设计与实现。

核心结构

单例模式的核心在于通过一个接口返回唯一的对象实例。

Java 设计模式 | 单例模式

常见写法

1、饿汉模式

public class Singleton {
​
    private Singleton() {
        System.out.println("create Singleton");
    }
​
    private static Singleton instance = new Singleton();
​
    public static Singleton getInstance(){
        return instance;
    }
}

饿汉模式单例的实现方式简单,在 JVM 对类加载的时候,单例对象就会被创建,因此线程安全。由于获取实例的静态方法没有使用同步方法,调用效率高。但如果该实例从始至终都没被使用过,则会造成内存浪费。

2、懒汉模式

public class LazySingleton {
​
    private LazySingleton() {
        System.out.println("create Singleton");
    }
​
    private static LazySingleton instance = null;
​
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

懒汉模式单例是对静态成员变量 instance 赋予初始值 null,确保系统启动时没有额外的负载。在第一次使用的时候才进行初始化,达到了懒加载的效果。由于获取实例的静态方法用 synchronized 关键字修饰,所以线程安全。但是由于每次获取实例都要进行同步加锁,因此效率较低。

3、双重检测机制(DCL)

public class DCLSingleton {
​
    private DCLSingleton() {
        System.out.println("create Singleton");
    }
​
    private static volatile DCLSingleton instance = null;
​
    public static DCLSingleton getInstance() {
        if (instance == null) {
            synchronized (DCLSingleton.class) {
                if(instance == null){
                    instance = new DCLSingleton();
                }
            }
        }
        return instance;
    }
}

双重检测机制(双重检查加锁)是在第一次使用的时候才进行初始化,达到了懒加载的效果。在进行初始化的时候会进行同步加锁,因此线程安全。并且只有第一次进行初始化才进行同步,因此不会有效率方面的问题。

CPU 内部会在保证不影响最终结果的前提下对指令进行重新排序(不影响最终结果只是针对单线程,切记),指令重排的主要目的是为了提高效率。

正常情况按顺序执行,双重检测机制是没有问题。如下:

memory = allocate();  // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
instance = memory;    // 3.设置 instance 指向刚才分配的内存地址

指令重排需后:

memory = allocate();  // 1.分配对象的内存空间
instance = memory;    // 3.设置 instance 指向刚才分配的内存地址
ctorInstance(memory); // 2.初始化对象

如果线程 A 执行完 1 和 3,instance 对象还未完成初始化,但是已经不再指向 null。此时线程 B 抢占到 CPU 资源,执行第12 行的检测结果为 false,则执行第19行,从而返回一个还未初始化完成的 instance 对象,从而出导致问题出现。

使用 volatile 关键字修饰 instance 对象可以禁止指令重排序。

4、静态内部类

public class StaticInnerHolderSingleton {
​
    private StaticInnerHolderSingleton(){
        System.out.println("create Singleton");
    }
​
    private static class InnerHolder{
        private static StaticInnerHolderSingleton instance = new StaticInnerHolderSingleton();
    }
    
    public static StaticInnerHolderSingleton getInstance(){
        return InnerHolder.instance;
    }
}

当 StaticInnerHolderSingleton 被加载时,内部类 InnerHolder 并不会被初始化,只有在 getInstance() 方法被调用时,才会加载 InnerHolder,从而初始化 instance,做到了延迟加载。

StaticInnerHolderSingleton 实例的创建在 Java 编译时期收集在 () 中,该方法又是同步方法,可以保证内存的可见性、JVM指令的顺序性以及原子性。

5、枚举

public enum EnumSingleton {
​
    INSTANCE;
​
    EnumSingleton(){
        System.out.println("create Singleton");
    }
​
    // 调用getInstance方法,事实上获得Holder的instance静态属性
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
​

枚举类型不允许被继承,同时是线程安全且只能被实例化一次,但是枚举类型不能够懒加载,对 EnumSingleton 主动使用,如用其中的静态方法 INSTANCE 会立即实例化。

通过 Java 反射机制或序列化和反序列化可能会破坏单例,但枚举模式的单例天然不存在这个问题。

单例破坏问题

通过 Java 反射机制,强行调用单例类的私有构造函数可以生成多个单例示例,这种情况相对极端,代码中也不会去如此实现。

对于序列化和反序列化,可以通过私有方法 readResolve() 解决这个问题,代码如下:

public class SerializableSingleton implements Serializable {
​
    private SerializableSingleton() {
        System.out.println("create Singleton");
    }
​
    private static SerializableSingleton instance = new SerializableSingleton();
​
    public static SerializableSingleton getInstance(){
        return instance;
    }
​
    private Object readResolve(){
        return instance;
    }
}
​

测试代码如下,可以自行测试:

public static void main(String[] args) throws Exception {
        SerializableSingleton s1  = null;
        SerializableSingleton s = SerializableSingleton.getInstance();
        // 先将实例序列化到文件
        FileOutputStream fos = new FileOutputStream("SerializableSingleton.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s);
        oos.flush();;
        oos.close();
        // 从文件反序列化读出原有的单例类
        FileInputStream fis = new FileInputStream("SerializableSingleton.txt");
        ObjectInputStream ois = new ObjectInputStream(fis);
        s1 = (SerializableSingleton) ois.readObject();
        System.out.println(s.equals(s1));
    }

事实上,在实现了私有的 readResolve() 方法后,readObject() 方法就已经形同虚设,它直接使用 readResolve() 替换了原本的返回值,从而从形式上构造了单例。

总结

在实际工作中,单例的使用还是比较常见的,在几种实现方式中,双重检测机制、静态内部类、枚举方式都是比较推荐。