Java 实现单例模式

单例模式简介

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

双重检查锁

为了在多线程环境下,不影响程序的性能,不让线程每次调用 getInstanceC() 方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在。

public class Singleton1 {
    private static volatile Singleton1 instance;

    private Singleton1() {}

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

优缺点

  • 优点

    • 延迟初始化:和懒汉模式一致,只有在初次调用静态方法 getInstance,才会初始化 Singleton1 实例。
  • 缺点

    • 性能优化:同步会造成性能下降,在同步前通过判读 instance 是否初始化,减少不必要的同步开销。

    • 线程安全:同步创建 Singleton1 对象,同时,注意到静态变量 instance 需要使用 volatile 修饰

延迟加载模式(Initialization-on-demand holder idiom)

加载一个类时,其内部类不会同时被加载。

一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。 由于在调用 Singleton2.getInstance() 的时候,才会对单例进行初始化。并且,通过反射,是不能从外部类获取内部类的属性的。

由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。

public class Singleton2 {
    private Singleton2() {

    }

    public static Singleton2 getInstance() {
        return ContextHolder.getInstance();
    }

    private static class ContextHolder {
        private static final Singleton2 INSTANCE = new Singleton2();
        public static Singleton2 getInstance() {
            return INSTANCE;
        }
    }
}

优缺点

  • 优点:

    • 实现代码简洁:与双重检查单例模式对比,静态内部类单例实现代码更简洁、清晰。

    • 延迟初始化:调用 getInstance 才初始化 Singleton2 对象。

    • 线程安全:JVM 在执行类的初始化阶段,会获得一个可以同步多个线程对同一个类的初始化的锁。

  • 缺点:

    • 需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。

饿汉模式

饿汉模式是通过 JVM 在加载类的时候,就完成类对象的创建:

public class Singleton3 {
    private static final Singleton3 INSTANCE = new Singleton3();

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        return INSTANCE;
    }

}

优缺点

  • 优点:JVM 层面的线程安全

    The semantics of Java guarantee that the field will not be initialized until the field is referenced, and that any thread which accesses the field will see all of the writes resulting from initializing that field. -- 摘自《The "Double-Checked Locking is Broken" Declaration》

    Java 的语义保证了在引用该字段之前,该字段不会被初始化,并且访问该字段的任何线程,都将看到初始化该字段所产生的所有写入。

  • 缺点:造成空间浪费

    饥饿模式是典型的以空间换时间思想的实现: 不用判断就直接创建, 但创建之后如果不使用这个实例, 就造成了空间的浪费. 虽然只是一个类实例, 但如果是体积比较大的类, 这样的消耗也不容忽视.

枚举方式

创建枚举默认就是线程安全的,所以不需要担心 double checked locking,而且,还能防止反序列化,导致重新创建新的对象。保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)。

public enum Singleton4 {
    INSTANCE(1, "car");

    private Integer id;
    private String name;

    private Singleton4(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    // ... 省略了getter/setter 方法
}

枚举单例模式的线程安全同样利用静态内部类中讲到类初始化锁。枚举单例模式能够在序列化和反射中保证实例的唯一性。

优缺点

  • 优点

    • 不需要考虑序列化的问题:

      枚举对象的序列化是由 JVM 保证的, 每一个枚举类型和枚举变量在 JVM 中都是唯一的, 在枚举类型的序列化和反序列化上 Java 做了特殊的规定: 在序列化时,Java 仅仅是将枚举对象的 name 属性输出到结果中, 反序列化时只是通过 java.lang.Enum#valueOf() 方法来根据名字查找枚举对象。编译器不允许对这种序列化机制进行定制、并且禁用了 writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve 等方法, 从而保证了枚举实例的唯一性。

    • 不需要考虑反射的问题:

      在通过反射方法 java.lang.reflect.Constructor#newInstance() 创建枚举实例时, JDK 源码对调用者的类型进行了判断:

      // 判断调用者clazz的类型是不是Modifier.ENUM(枚举修饰符), 如果是就抛出参数异常:
      if ((clazz.getModifiers() & Modifier.ENUM) != 0)
           throw new IllegalArgumentException("Cannot reflectively create enum objects");
      
  • 缺点: 所有的属性都必须在创建时指定, 也就意味着不能延迟加载; 并且使用枚举时占用的内存比静态变量的2倍还多, 这在性能要求严苛的应用中是不可忽视的.


参考:

posted @ 2024-01-16 10:53  LARRY1024  阅读(28)  评论(0编辑  收藏  举报