[Java] 细数几种单例的实现方式

前言


单例模式是比较常见且常用的设计模式之一,Java中有诸多种单例设计模式的实现。现在笔者将在本文盘点在Java中实现单例模式的几种常见的方法。

参考


  1. Atomic Access - oracle docs
  2. 《Effective Java 3rd edition》 - Item 3: Enforce the singleton property with a private constructor or an enum type
  3. Serialization of Enum Constants - oracle docs

什么是单例模式?


在介绍几种单例模式的实现之前,笔者想先澄清一下单例模式的定义。

单例模式是一种设计模式,也是最为简单的设计模式。在OOP里,当我们在逻辑上认为某类应该只有一个实例的时候,我们采用此设计模式,下面笔者简单列举一下我们也许能用到单例模式的地方(※非必须)。

  • Logger类:统一写log的入口。
  • 应用Configuration类:你的应用程序不会想要多个存放配置信息的配置类实例。
  • 保存数据的仓库类:如数据生产者提供的消息(数据),保存在消息仓库里。这时你不应该提供多个仓库类的实例,因为这些数据应该被放在同一个仓库里。

JDK里的单例类


笔者在本节列举了一些在JDK里使用单例模式的类以供参考。有兴趣的读者可以自行在开源JDK里寻找其对应源码,参考其单例实现。

  • java.lang.Runtime
  • java.awt.Desktop
  • java.lang.SecurityManager.java

单例模式的实现


单例模式在java中有诸多实现,各类实现也有各自不同的优缺点。
这里的特点主要有

  • 懒汉实现与非懒汉
  • 线程安全与非线程安全
  • 并发高性能与并发低性能
  • 序列化・反序列化的安全与否
  • 是否抗反射攻击

笔者在下表列出几种单例模式的基本信息。并将在后面的子章节说明每一种实现的特点并提供其示例代码。

No. 实现 EN关键词 线程安全 Lazy-Loading 高并发性能 序列化・反序列化安全性 抗反射攻击(※1)
1 非懒汉实现 Eager initialization
2 懒汉实现 Lazy initialization
3 同步懒汉实现 Synchronized Lazy initialization
4 双重检查锁定 Double checked locking
5 静态内部类实现 Bill Pugh Singleton Implementation
6 枚举实现 Enum Singleton

※1. 抗反射攻击:对Java来说,私有化构造器只能放置在编译时,被外部调用,却并不能阻止通过反射(reflection)API AccessibleObject.setAccessible 来在运行时改变其访问控制。

1. 非懒汉实现 (Eager initialization)


笔者第一次看到Eager的时候也是很懵,不知道如何翻译。有人说Eager理解为Preloading,笔者觉得这个替换不错。Preloading是预先加载,也就是本节标题的非懒汉实现了。
也有别的地方称这实现为饿汉实现

其具体实现如下:

/**
 * 利用静态初始化在class load阶段初始化单例实例。
 */
public class EagerInstantiationSingleton {

    /**
     * 私有类成员变量,用于存放唯一实例。
     */
    private static EagerInstantiationSingleton sharedInstance = new EagerInstantiationSingleton();

    /**
     * 构造器私有化。
     */
    private EagerInstantiationSingleton() {}

    /**
     * 公有类方法,获取唯一实例。
     */
    public static EagerInstantiationSingleton getInstance() {
        return sharedInstance;
    }
}

这种实现会在类加载的时候,利用静态初始化初始化其唯一实例。这种实现非常简单,当你的单例类不占用大量资源的时候,可以使用这种简单的单例实现方式。

而当你的单例类占用大量计算机资源的时候,你就需要考虑使用懒汉实现,来延迟其初始化的时间。

2. 懒汉实现 (Lazy initialization)


经典的不加锁的懒汉实现,与上述的非懒汉实现相比起来,这种实现有延迟加载的特点,可以延迟实例的初始化直到client(调用方)调用getInstance()方法时才初始化。但因不加锁,在高并发的情况下,很可能会出现被多次实例化的情况(也就是所谓的线程不安全)。

在多线程程序里,并不推荐使用这种实现,但当你的程序只有单线程时,请放心大胆使用。

其具体实现如下:

/**
 * 经典单例实现
 */
public class ClassicSingleton {

    /**
     * 私有类成员变量,用于存放唯一实例。
     */
    private static ClassicSingleton sharedInstance;

    /**
     * 构造器私有化。
     */
    private ClassicSingleton() {}

    /**
     * 公有类方法,获取唯一实例。
     */
    public static ClassicSingleton getInstance() {
        if (sharedInstance == null)
            sharedInstance = new ClassicSingleton();
        return sharedInstance;
    }
}

3. 同步懒汉实现(Synchronized Lazy initialization)


上面在懒汉实现一节,我们提到了懒汉实现并不是线程安全的,
为了解决线程不安全的问题,有了第一次尝试改进(即:并非最终解决方案),这便是本实现。

这个实现虽然解决了线程安全的问题,但却引入了新的问题,那就是因同步块作用域范围过大,导致性能急剧下降,在高并发场景,这显然是不可取的,所以作为一种不上不下的方案,读者们只要知道有这种实现即可。

其具体实现如下:

/**
 * 利用synchronized同步关键字实现,
 * 线程安全的传统单例。
 */
public class ThreadSafeClassicSingleton {
    /**
     * 私有类成员变量,用于存放唯一实例。
     */
    private static ThreadSafeClassicSingleton sharedInstance;

    /**
     * 构造器私有化。
     */
    private ThreadSafeClassicSingleton() {}

    /**
     * 同步公有类方法,获取唯一实例。
     */
    public static synchronized ThreadSafeClassicSingleton getInstance() {
        if (sharedInstance == null)
            sharedInstance = new ThreadSafeClassicSingleton();
        return sharedInstance;
    }
}

如果你不知道什么叫同步块作用域过大,请看
If i synchronized two methods on the same class, can they run simultaneously? - Stack overflow
静态同步方法与实例同步方法类似,使用的锁对象是其Class类的实例(ThreadSafeClassicSingleton.class)。其作用范围是该类所有的同步类方法

4. 双重检查锁定实现 (Double checked locking / DCL)


在上一节,我们提到了同步懒汉实现的问题

“因同步块作用域范围过大,导致性能急剧下降”

而双重检查锁定实现则解决这一问题。通过缩小同步块作用范围,让同步块仅作用于创建实例的部分,而不是整个方法。

  1. 第一重检查很好的规避了初始化成功后的抢锁操作,提高了性能。

  2. 第二重检查则是为了让刚开始未初始化成功时,避免同时抢锁的多个线程多次创建实例,这也许难以理解,第一个线程执行结束之后,其他等待锁的线程会依次进入该同步块,所以第二重检查是为了阻止除第一个被执行的线程之外的线程创建实例。

  3. volatile关键字,是为了保证让其他线程对sharedInstance被写入操作的可见性。即当一个线程写入数据到sharedInstance时,其他线程能直接获取到最新的sharedInstance的值,而非缓存的值。

这种实现是面试中常常被问到的一种实现。

具体实现如下:

/**
 * 缩小同步块影响范围的高速线程安全的懒汉单例实现
 */
public class DoubleCheckedLockingSingleton {
    /**
     * 私有类成员变量,用于存放唯一实例。
     * volatile是关键,保证了高并发时,其他线程对初始化线程写入操作的可见性。
     * Ref - "... This means that changes to a volatile variable are always visible to other threads."
     * https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
     */
    private volatile static DoubleCheckedLockingSingleton sharedInstance;

    /**
     * 构造器私有化。
     */
    private DoubleCheckedLockingSingleton() {}

    public DoubleCheckedLockingSingleton getInstance() {
        // 一重检查:除了最开始同时抢锁的那些线程之外,后续进入到本方法的线程都会被第一次检查挡住,直接返回现有实例。
        if (sharedInstance == null) {
            // 抢锁,高并发时,同时会有多个线程会同时抢class对象的锁。只有一个线程能抢到锁并开始初始化。
            synchronized (DoubleCheckedLockingSingleton.class) {
                // 二重检查,当一个线程完成初始化操作之后,退出同步块,释放class的重入锁。剩余等待锁的线程便能一个一个进入
                // 到这个同步块,并由第二次检查,放置这些线程再次初始化。
                if (sharedInstance == null)
                    sharedInstance = new DoubleCheckedLockingSingleton();
            }
        }

        return sharedInstance;
    }

}

5. 静态内部类实现 (Bill Pugh Singleton Implementation)


在老版Java里,因其内存模型(memory model)有诸多问题,虽然这些个问题通过JSR-133得以修正成我们现在所熟知的新版内存模型上,但在那之前,有一个叫Bill Pugh这个人想出了一种利用JVM类加载处理是线程安全的这一特性,来回避内存模型问题的实现。

这个实现利用了类加载机制,来保证线程安全性。还利用类延迟加载的特性,延迟加载内部helper类(也叫lazy holder类),实现了懒汉初始化。

这个笔者觉得非常巧妙,只不过会多生成一个class文件。但可以说这个实现非常简单易懂,相比起DCL实现而言。

其具体实现如下:


public class BillPughSingleton {

    /**
     * 构造器私有化。
     */
    private BillPughSingleton() {}

    /**
     * 内部静态helper类 (inner static helper class)
     */
    private static class SingletonHelper {
        private static final BillPughSingleton SHARED_INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.SHARED_INSTANCE; // 只有第一次访问getInstance才回去加载SingletonHelper类,高并发时
    }
}

6. 枚举实现 (Enum Singleton)


枚举实现,利用Java语言Enum类型抗反射和JVM类加载是线程安全的这两个特性,使得其永远只可能有一个实例并且是懒汉加载的(只有当第一次被使用才会加载)。

Java编译器禁止了对Enum类型的反射,也使其具有了抗反射攻击的特性。

Enum类型本身也因其特殊的序列化和反序列化的过程,能直接解决序列化・反序列化造成的多个实例被创建的问题。

这种实现,非常地简单,易懂易实现,这大概也是《Effective Java》的作者非常推崇这种写法的原因之一吧。

延伸阅读:Serialization of Enum Constants - oracle docs

其具体实现如下:

public enum EnumSingleTon {
    SHARED_INSTANCE;

    public static void yourMethod() {
        // 做任何你想干的事情。
    }
}

这种实现被《Effective Java》的作者认为是目前最好的Singleton的实现。

This approach may feel a bit unnatural, but a single-element enum type is often the best way to implement a singleton.
Note that you can’t use this approach if your singleton must extend a superclass other than Enum.

结语


虽然单例模式作为一个简单的设计模式,却也有许多种实现,笔者认为每一种实现都有其存在的道理,也有其解决的问题。希望本文能帮你理清各个实现和其之间的优化关系。

而在如Spring等DI框架大行其道的时候,我们系统的各个组件(模块),则可以由这些DI框架来保证在运行期间仅被创建一个实例,不过这就是另一个话题了。


因篇幅和这两种case实属罕见的原因,笔者没有在本文写下关于以下两个问题的解决方案。

  • 如何使你的Singleton实现具有序列化・反序列化安全性?
  • 如何使你的Singleton实现能抗反射攻击?

如果有兴趣的读者可以自行查阅。

发布了24 篇原创文章 · 获赞 24 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ToraNe/article/details/103863743
今日推荐