Java单例模式详解(附带实例)
单例模式能够确保某个类只有唯一的实例,并且让用户能够从任何地方便利地访问到该实例。
这个模式很早就在业界使用了,而且也是 GoF 设计模式之中的一种。
对于用户或应用程序来说,某些类在程序运行的时候只应该有唯一的实例。假如不这样限制,那么开发者可能会在无意间创建出该类的多个实例,但这些实例所使用的其实是同一个资源,并且该资源只有一个。由于这些实例都在使用这个资源,因此可能导致程序不够稳定。
单例模式能够保证某个类在当前运行的虚拟机里只有唯一的实例,并提供一个入口点,让开发者由此获取这个实例。
这个单例类是 Runtime 类,你可以通过它的 getRuntime() 方法获取单例,该类位于 java.base 模块的 java.lang 包里。
getRuntime() 方法返回一个与当前的 Java 应用程序相关联的对象。得到这个对象之后,用户可以在其上执行一些操作,例如,可以添加关闭挂钩(shutdown hook),以便在虚拟机关闭的时候触发。

图 1 如何用单例模式描述唯一的一辆车以及它的发动机
这个例子意味着某一种特定的车辆,以及该车辆的发动机在 JVM 里只有唯一的实例。
【实例】OnlyEngine 类与 OnlyCar 类在程序运行期间都分别只有唯一的一个实例。
有好几种办法可以确保某个类只有一个实例。下面这个 OnlyEngine 类采用的是惰性初始化方式,它会在真正需要用到实例的时候,再去创建这个实例(参见下面的实例)。OnlyEngine 类实现了通用的 Engine 接口。它提供一个静态的 getInstance() 方法,让用户通过该方法获取单例。
【实例】OnlyEngine类采用惰性初始化方式来实现单例模式。
还有一种实现单例的办法是把这个单例声明成类里的静态字段,并在声明的时候就进行初始化。这种办法跟刚才那种一样,也提供一个名为 getInstance() 的静态方法给用户去调用(参见下面的实例)。另外要注意,单例类的构造器应该设置为 private,以防止其他代码调用这个构造器,导致单例类出现多个实例。
【实例】OnlyVehicle类采用声明静态字段并初始化的方式来实现单例模式。
为了解决这个问题,可以考虑把单例类设计成枚举类,也就是 enum 类,并把单例声明成这个枚举类的唯一枚举常量,参见下面的实例。
【实例】OnlyEngineEnum类采用枚举类来实现单例模式。
单例类不太容易坚持单一责任原则(SRP),因为它既要确保自己只有一个实例,又要负责初始化这个实例。同时担负这两个职责也是有好处的,因为这样能够确保用户访问到的是唯一的这一个资源,而且能够防止用户无意间初始化该类的其他实例,或者无意间让该类的实例遭到回收。
单例模式必须审慎地使用,因为单例类会把初始化单例的这部分代码与本类紧密耦合起来,这可能会导致你很难分别测试这部分代码与单例类的其他代码。另外,单例模式还意味着单例类本身不应该有子类,这让我们很难对单例类进行扩展。
这个模式很早就在业界使用了,而且也是 GoF 设计模式之中的一种。
对于用户或应用程序来说,某些类在程序运行的时候只应该有唯一的实例。假如不这样限制,那么开发者可能会在无意间创建出该类的多个实例,但这些实例所使用的其实是同一个资源,并且该资源只有一个。由于这些实例都在使用这个资源,因此可能导致程序不够稳定。
单例模式能够保证某个类在当前运行的虚拟机里只有唯一的实例,并提供一个入口点,让开发者由此获取这个实例。
单例模式在JDK中的运用
JDK 里最经典的一种单例就是 Java 应用程序本身,更为准确地说,是正在运行着的这个 Java 应用程序所处的运行环境。这个单例类是 Runtime 类,你可以通过它的 getRuntime() 方法获取单例,该类位于 java.base 模块的 java.lang 包里。
getRuntime() 方法返回一个与当前的 Java 应用程序相关联的对象。得到这个对象之后,用户可以在其上执行一些操作,例如,可以添加关闭挂钩(shutdown hook),以便在虚拟机关闭的时候触发。
Java单例模式实例
我们举一个例子,假设某种车只有一辆,它里面有唯一的发动机(参见下图)。
图 1 如何用单例模式描述唯一的一辆车以及它的发动机
这个例子意味着某一种特定的车辆,以及该车辆的发动机在 JVM 里只有唯一的实例。
【实例】OnlyEngine 类与 OnlyCar 类在程序运行期间都分别只有唯一的一个实例。
public static void main(String[] args) { System.out.println("Singleton pattern: only one engine"); var engine = OnlyEngine.getInstance(); var vehicle = OnlyVehicle.getInstance(); vehicle.move(); System.out.println("\"" + "OnlyEngine:'%s', equals with vehicle:'%s'" + "\"", (engine.equals(vehicle.getEngine()))); }程序输出结果如下:
Pattern Singleton: only one engine
OnlyVehicle, move
OnlyEngine:'OnlyEngine@7e9e5f8a', equals with vehicle:'true'
有好几种办法可以确保某个类只有一个实例。下面这个 OnlyEngine 类采用的是惰性初始化方式,它会在真正需要用到实例的时候,再去创建这个实例(参见下面的实例)。OnlyEngine 类实现了通用的 Engine 接口。它提供一个静态的 getInstance() 方法,让用户通过该方法获取单例。
【实例】OnlyEngine类采用惰性初始化方式来实现单例模式。
interface Engine {} class OnlyEngine implements Engine { private static OnlyEngine INSTANCE; static OnlyEngine getInstance() { if (INSTANCE == null) { INSTANCE = new OnlyEngine(); } return INSTANCE; } private OnlyEngine() {} }
还有一种实现单例的办法是把这个单例声明成类里的静态字段,并在声明的时候就进行初始化。这种办法跟刚才那种一样,也提供一个名为 getInstance() 的静态方法给用户去调用(参见下面的实例)。另外要注意,单例类的构造器应该设置为 private,以防止其他代码调用这个构造器,导致单例类出现多个实例。
【实例】OnlyVehicle类采用声明静态字段并初始化的方式来实现单例模式。
class OnlyVehicle { private static OnlyVehicle INSTANCE = new OnlyVehicle(); static OnlyVehicle getInstance() { return INSTANCE; } private OnlyVehicle() { this.engine = OnlyEngine.getInstance(); } private final Engine engine; void move() { System.out.println("OnlyVehicle, move"); } Engine getEngine() { return engine; } }用惰性初始化方式实现单例模式在多线程环境下可能会出问题,因为你必须设法对提供单例的 getInstance() 方法做出适当的同步,以防止多个线程分别创建出不同的实例。
为了解决这个问题,可以考虑把单例类设计成枚举类,也就是 enum 类,并把单例声明成这个枚举类的唯一枚举常量,参见下面的实例。
【实例】OnlyEngineEnum类采用枚举类来实现单例模式。
enum OnlyEngineEnum implements Engine { INSTANCE; } ... private OnlyVehicle() { this.engine = OnlyEngineEnum.INSTANCE; } ...
总结
单例模式是一种相对来说比较简单的设计模式,只有在多线程的环境下才会因为防止这些线程分别创建出不同的实例而变得稍微有点复杂。单例类不太容易坚持单一责任原则(SRP),因为它既要确保自己只有一个实例,又要负责初始化这个实例。同时担负这两个职责也是有好处的,因为这样能够确保用户访问到的是唯一的这一个资源,而且能够防止用户无意间初始化该类的其他实例,或者无意间让该类的实例遭到回收。
单例模式必须审慎地使用,因为单例类会把初始化单例的这部分代码与本类紧密耦合起来,这可能会导致你很难分别测试这部分代码与单例类的其他代码。另外,单例模式还意味着单例类本身不应该有子类,这让我们很难对单例类进行扩展。