云计算百科
云计算领域专业知识百科平台

设计模式之单例模式

本文中涉及到的完整代码存放于以下 GitHub 仓库中 LearningCode

1. 理论部分

单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

1.1 结构与实现

单例模式只包含一个角色 —— Singleton:

  • 职责:负责创建它自己的唯一实例,并允许外部访问它的唯一实例。
  • 实现:Singleton 一般声明为类。Singleton 在内部定义一个类型为 Singleton 的静态对象,用于指向供外部共享访问的唯一实例。为了防止在外部对单例类实例化,Sington 会将其构造函数的可见性设为私有。Singleton 会在其内部创建它的唯一实例,并通过类方法 getInstance() 让客户端使用它的唯一实例。

以唯一实例的创建时机进行区分,单例模式分为以下两种:

  • 饿汉式单例
  • 懒汉式单例

1.1.1 饿汉式单例

饿汉式单例类在类被加载时就将自己实例化:

  • 优点:实现简单,无须考虑多个线程同时访问的问题,可以确保实例的唯一性。
  • 缺点:无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,在一定程度上会导致资源浪费。

饿汉式单例在实现上可以采用的方式有:静态赋值、静态代码块、枚举类等。以下展示的饿汉式单例的 UML 类图就是采用静态赋值的方式: 在这里插入图片描述

1.1.2 懒汉式单例

懒汉式单例类在 getInstance 方法第一次被调用时将自己实例化:

  • 优点:实现了延迟加载。
  • 缺点:实现复杂,需要处理多个线程同时访问的问题。

懒汉式单例在实现上可以采用的方式有:锁控制,IoDH(依赖于语言特性,例如 Java 中的静态内部类)。

以下展示的懒汉式单例的 UML 类图省略了处理多个线程并发访问的代码: 在这里插入图片描述

1.2 扩展:创建单例类的子类

当唯一实例的类型可能是单例类,也可能是单例类的子类时,即需要获取单例类及其子类的唯一实例,需要对单例模式进行扩展。实现上述目标一般有以下 3 种方式:

  • 在 getInstance 方法中硬编码所有可能的单例类,通过环境变量或配置文件的方式动态指定单例类。
    • 优点:实现简单。
    • 缺点:硬性限定了可能的单例类集合,不够灵活。
  • 在编译时链接指定单例类。
    • 优点:不需要在 getInstance 方法中硬编码所有可能的单例类。
    • 缺点:难以在运行时选择单例类,不够灵活。
  • 使用单件注册表:
    • 优点:将唯一实例的选择权交给了外部,因此可以在运行时选择单例类,足够灵活。
    • 缺点:实现复杂,需要设计一种注册机制供单例类使用。

在不考虑反射的情况下,上述单例类及其子类的构造方法的可见性不一定是私有,否则可能无法创建唯一实例。

重点介绍单件注册表。单件注册表通常是一个全局唯一的组件,内部维护一个私有键值对,用于在唯一标识符 key与对应的单例实例之间建立映射关系。单件注册表一般会提供两类核心操作,一类操作是根据 key 查找指定唯一实例,另一类操作是向注册表中注册新的唯一实例;除此之外,单件注册表还可以提供替换、删除等操作。根据注册行为的发起者不同,向单件注册表注册单例的方式可分为以下两类:

  • 单例类可以在类加载阶段主动向注册表注册自身。
  • 客户端在运行时显式注册所需单例子类。

基于单件注册表进行扩展的单例模式的通用 UML 类图如下所示: 在这里插入图片描述

1.3 扩展:多例模式

多例模式是单例模式的扩展,允许存在多个实例,但这些实例的数量是有限制的,并且每个实例都通过一个唯一的键来标识和访问。 与单例模式一样,多例模式根据实例的创建时机分为饿汉式与懒汉式。 多例模式的 UML 类图如下所示: 在这里插入图片描述

1.4 优缺点与适用场景

单例模式具有以下优点:

  • 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
  • 与全局变量相比,单例模式避免了那些存储唯一实例的全局变量污染命名空间。
  • 与类方法相比,单例模式支持通过子类化扩展其功能,从而允许在不修改原有实现的前提下灵活重定义其行为。
  • 允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。

单例模式存在以下缺点:

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法,将对象的创建和对象本身的功能耦合在一起。

单例模式适用于以下场景:

  • 当类只能有一个实例而且系统调用类的单个实例只允许使用一个公共访问点时。
  • 当系统所需的唯一实例应该是通过子类化可扩展的,并且客户应该无须更改代码就能使用一个扩展的实例时。

2. 实现部分

以 Java 代码为例,演示单例模式及其扩展的实现。

2.1 Java 5 种单例模式的实现

请参考文章 单例模式与 Volidate的学习 —— 我爱学习呀

2.2 扩展:创建单例类的子类

案例介绍: 在这里插入图片描述

2.2.1 方式一

getInstance 中硬编码所有可能的单例类。

public abstract class LoadBalancer {
private static volatile LoadBalancer INSTANCE;
private static final String ENV_TYPE = "Load_Balancer_TYPE";

protected LoadBalancer() {

}

public static LoadBalancer getInstance() {
LoadBalancer localRef = INSTANCE;
if (localRef == null) {
synchronized (LoadBalancer.class) {
localRef = INSTANCE;
if (localRef == null) {
String className = System.getenv(ENV_TYPE);
if (className == null) {
throw new IllegalArgumentException("Environment variable not set or invalid: " + ENV_TYPE);
}
localRef = switch (className) {
case "ROUND" -> new RoundLoadBalancer();
case "RANDOM" -> new RandomLoadBalancer();
case "LEAST_CONNECTION" -> new LeastConnectionLoadBalancer();
case "IP_HASH" -> new IpHashLoadBalancer();
default -> throw new IllegalArgumentException("Unsupported load balancer type: " + className);
};
INSTANCE = localRef;
}
}
}

return localRef;
}

public abstract void addServer(String server);

public abstract void removeServer(String server);

public abstract String getServer();
}

2.2.2 方式二

Java 中不能像 C++ 一样在编译时将子类的类型绑定到唯一实例上,这里使用 ServiceLoader 机制来模仿该行为。

import java.util.ServiceLoader;

public abstract class LoadBalancer {
private static volatile LoadBalancer INSTANCE;

public static LoadBalancer getInstance() {
LoadBalancer localRef = INSTANCE;
if (localRef == null) {
synchronized (LoadBalancer.class) {
localRef = INSTANCE;
if (localRef == null) {
ServiceLoader<LoadBalancer> loader = ServiceLoader.load(LoadBalancer.class);
localRef = loader.findFirst()
.orElseThrow(() -> new IllegalStateException("No LoadBalancer implementation found"));
INSTANCE = localRef;
}
}
}

return localRef;
}

public abstract void addServer(String server);

public abstract void removeServer(String server);

public abstract String getServer();
}

2.2.3 方式三

使用单件注册表。该设计方案为:父类 LoadBalancer 仅提供一个 getInstance 作为唯一实例的全局访问点, 子类在静态代码块中向注册表注册自身,外部客户端加载指定子类即可在运行期间指定唯一实例的类型。

单例类代码如下:

public abstract class LoadBalancer {
protected static final String KEY = "org.example.case03.LoadBalancer";

public static LoadBalancer getInstance() {
return SingletonRegistry.get(KEY);
}

public abstract void addServer(String server);

public abstract void removeServer(String server);

public abstract String getServer();
}

单件注册表代码如下:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class SingletonRegistry {
private static final ConcurrentMap<String, Object> INSTANCE_MAP = new ConcurrentHashMap<>();

public static void put(String key, Object instance) {
INSTANCE_MAP.put(key, instance);
}

@SuppressWarnings("unchecked")
public static <T> T get(String key) {
return (T) INSTANCE_MAP.get(key);
}
}

完整的 UML 类图如下所示: 在这里插入图片描述

2.3 扩展:多例模式

演示以下 2 种多例模式的编写:

  • 饿汉式
  • 懒汉式

2.3.1 饿汉式

某公司希望为几个固定部门(如 HR、IT、Finance)预分配打印机资源,以确保每次请求都能快速获取打印机。因此,决定使用饿汉式多例模式设计一个打印机管理系统。该系统应满足以下要求:

  • 在系统启动时,预先创建好每个部门的打印机实例。
  • 所有打印机实例存储在一个注册表中。
  • 每个部门只能拥有一个打印机实例(通过部门名称唯一标识)。
  • 请求打印机时,直接从注册表中获取,不重新创建。
  • 系统支持固定的几个部门,不支持动态添加。

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class PrinterManager {
private static final ConcurrentMap<String, Printer> MAP = new ConcurrentHashMap<>();

static {
MAP.put("人力资源部", new Printer("人力资源部"));
MAP.put("财务部", new Printer("财务部"));
MAP.put("销售部", new Printer("销售部"));
}

private PrinterManager() {

}

public static Printer getInstance(String departmentName) {
Printer printer = MAP.get(departmentName);
if (printer == null) {
throw new RuntimeException("No printer is available for department: " + departmentName +
". Only predefined departments are supported.");
}
return printer;
}
}

2.3.2 懒汉式

在一家大型公司中,每个部门(如 HR、IT、Finance)都需要一台打印机。为了节省系统资源,公司决定使用懒汉式多例模式设计一个打印机管理系统。该系统应满足以下要求:

  • 每个部门只能拥有一个打印机实例(通过部门名称唯一标识)。
  • 打印机实例应按需创建,即只有在第一次请求该部门的打印机时才创建。
  • 所有部门的打印机都通过一个统一的管理类来获取。
  • 系统支持多个部门,但每个部门的打印机实例只能有一个。
  • 系统需保证线程安全。

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class PrinterManager {
private static final ConcurrentMap<String, Printer> MAP = new ConcurrentHashMap<>();
private static final int MAX_PRINTER_INSTANCES = 2;

private PrinterManager() {

}

public static Printer getInstance(String departmentName) {
Printer printer = MAP.get(departmentName);
if (printer == null) {
synchronized (PrinterManager.class) {
printer = MAP.get(departmentName);
if (printer == null) {
if (MAP.size() < MAX_PRINTER_INSTANCES) {
printer = new Printer(departmentName);
MAP.put(departmentName, printer);
}else {
throw new RuntimeException("Department '" + departmentName + "' cannot get a printer now. " +
"All printers are in use. Please wait…");
}
}
}

}

return printer;
}

public static void releasePrinter(String departmentName) {
Printer removed = MAP.remove(departmentName);
if (removed != null) {
System.out.println("Printer released by department: " + departmentName);
} else {
System.out.println("No printer was assigned to department: " + departmentName);
}
}
}

参考资料

学习视频:

  • 设计模式快速入门 —— 图灵星球TuringPlanet —— 单例模式
  • Java设计模式详解 —— 黑马程序员 —— 单例模式(P22 ~ P33)
  • Java设计模式 —— 尚硅谷 —— 单例模式(P29 ~ P38)
  • 学习读物:

  • 《设计模式:可复用面向对象软件的基础》—— Erich Gamma 著 —— 李英军 译 —— 第 3.5 节(P96)
  • 《Java 设计模式》 —— 刘伟 著 —— 第 8 章(P86)
  • 《设计模式之美》—— 王争 著 —— 第 6.1 节(P167)
  • 《设计模式之禅》 —— 第 2 版 —— 秦小波 著 —— 第 7 章(P58)
  • 《图解设计模式》—— 结城浩 著 —— 杨文轩 译 —— 第 5 章(P43)
  • 电子文献:

  • 设计模式教程 —— 菜鸟教程 —— 单例模式
  • Threadsafe Singleton Design Pattern Java —— Upasana
  • 单例模式与 Volidate的学习 —— 我爱学习呀
  • 99+ 种软件模式 —— long2ge —— 单例模式
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 设计模式之单例模式
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!