? ? ?在软件开发的赛场上,单例模式以其独特的魅力长期占据着重要的地位。作为设计模式中的一员,它在整个软件工程的棋盘上扮演着关键性角色。本文将带你深入探索单例模式的神秘面纱,从历史渊源到现代应用,从基础实现到高级技巧,经过戏剧性的转折和层层推进,我们将一步步揭开这一模式背后的秘密。
? ? ?首先,文章将串起时间的线索,带你重回单例模式的起源,理解它在软件工程历史中的地位。经过时间的流逝,单例模式不仅保持了其原有的魅力,而且随着新的编程语言和技术的发展,还展示出了新的活力和应用场景。
? ? ?接着,细致解码单例模式的基础结构,为你展现其底层的实现方式。通过对不同实现策略的探讨,我们会评估各自的优势和潜在风险,帮助你作出明智的设计选择。无论是饿汉式的立即加载,还是懒汉式的延迟兴建,或是静态内部类和枚举方式的现代演化,本文将在详尽的案例分析中,为你搭建一座走向单例模式深入理解的桥梁。
? ? ?文中还将精心打造实战场景,凸显单例模式在不同应用中的策略运用。从多线程的并发控制到资源访问的优化,从全局状态管理到服务类的持久化,单例模式如何巧妙地解决现实问题——这些都将在本文中一一揭晓。
? ? ?在“单挑编程世界”的冒险中,你将体验到单例模式的实践力量。面对代码的战场,这篇文章将成为你的秘密武器,助你披荆斩棘,挖掘单例模式背后藏匿的珍宝。无论你是设计模式的新手,还是寻求更高级理解的资深开发者,本文都将与你并肩作战,一起探索编程世界的深奥与精彩。
目录
? ? ?单例模式的历史渊源可以追溯到上世纪90年代,当时设计模式的概念由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides等人在《设计模式:可复用面向对象软件的基础》一书中首次提出。
? ? ?最初的设计模式书籍中并没有明确提到单例模式,但许多后来的书籍和文章将其作为设计模式的一部分进行讨论和描述。单例模式的概念是为了解决在软件开发中需要创建一个全局唯一对象的问题。
? ? ?随着编程语言和技术的发展,单例模式逐渐成为软件工程领域的重要模式之一,并广泛应用于各种类型的应用程序和系统中。它在许多领域具有实际应用,包括但不限于以下几个方面:
? ? ?1. 资源管理:单例模式常用于管理共享资源,例如数据库连接池、线程池、日志管理器等。通过使用单例模式,可以确保在整个应用程序中只有一个实例存在,从而有效地管理资源和提高性能。
? ? ?2. 配置管理:单例模式在配置管理中也扮演着重要角色。例如,一个应用程序可能需要读取配置文件中的信息,并在整个应用程序中共享这些配置信息。通过使用单例模式,可以确保在整个应用程序生命周期中只有一个配置对象存在。
? ? ?3. 全局状态管理:在某些情况下,需要在应用程序中维持一些全局状态数据,例如游戏中的玩家信息、应用程序的运行时状态等。通过使用单例模式,可以方便地访问和更新这些全局状态数据,并确保数据的一致性和唯一性。
? ? ?4. 日志记录:单例模式常用于日志记录器的实现。通过使用单例模式,可以确保在整个应用程序中只有一个日志记录器实例,从而统一管理和记录日志信息。
? ? ?5. 缓存管理:在需要缓存数据的场景中,单例模式可以用来实现缓存管理器。通过单例模式,可以确保整个应用程序中只有一个缓存管理器实例,提高缓存的效果和一致性。
? ? ?6. 节约内存和提高性能:由于单例模式只创建一个实例,可以减少内存占用,并且在运行时能够提供更高的性能。这在需要频繁创建和销毁对象的场景中尤为重要,如线程池、网络连接等。
? ? ?7. 限制实例化和控制访问:单例模式可以限制一个类只能创建一个实例,并提供一个统一的访问点。这可以防止其他对象直接实例化该类,从而增加了对实例化过程的控制和安全性。
? ? ?8. 模块化和解耦合:单例模式可以帮助实现模块化和解耦合,在应用程序中以模块的形式组织和管理代码。单例模式将整个模块封装在一个类中,对外暴露统一的接口,使得模块之间的交互更清晰和简化。
????????
? ? ?场景:银行推出一个新的产品(就叫新产品了),通过获取个人缴纳部分的公积金额度乘以某倍数得到的总金额来作为准入的其中一个规则和计算授信额度的标准。
? ? ? ? ...
? ? ?最终,就是调某个接口(叫外部产品了)来获取客户的公积金信息。外部产品SDK说明第一步是实例一个Client,由Client负责组装数据、加密(解密)等然后发post请求来获取公积金信息。????????
????????
? ? ?考虑到是尝试新产品和部分地区尝试上线、硬件软件、服务器性能情况等综合因素考虑选择使用懒汉式来实例化产品Client。
? ? ?实现示例代码:
public class LazySingleton {
private static SdkClient sdkClient;
private LazySingleton() {
// 私有构造函数
}
public static synchronized SdkClient getInstance() {
if (sdkClient == null) {
sdkClient = new SdkClient();
}
return sdkClient;
}
}
? ? ?在懒汉模式中,构造函数是私有的,确保外部无法直接实例化该类。getInstance() 方法返回单例对象的实例,并在首次调用时创建对象。?
? ? ?优点:
? ? ?缺点:
?????????
? ? ?静态内部类方式利用Java的类加载机制来实现懒汉式的延迟加载。
? ? ?实现示例代码:
public class StaticInnerSingleton {
private StaticInnerSingleton() {
// 私有构造函数
}
private static class SingletonHolder {
private static final SdkClient instance = new SdkClient();
}
public static SdkClient getInstance() {
return SingletonHolder.instance;
}
}
? ? ?在这个示例中,私有的构造函数确保了外部无法直接实例化该类的对象。通过静态内部类SingletonHolder
来持有单例对象,并且该类的实例化操作是在静态初始化阶段(即类加载时)完成的,保证了线程安全性。
? ? ?优点:
懒加载:单例对象的实例化在调用getInstance()
方法时才会执行,实现了延迟加载的效果。
线程安全:由于静态内部类的特性,当多个线程同时访问getInstance()
方法时,静态内部类的实例化操作只会执行一次,保证了线程安全性。
简洁性:相对于双重检查锁等方式,使用静态内部类实现单例模式的代码更加简洁明了。
? ? ?缺点:
需要了解并理解静态内部类的工作原理:虽然使用静态内部类实现单例模式代码相对简洁,但是理解这种实现方式的工作原理可能需要一定的知识和经验。对于不熟悉静态内部类的开发者来说,可能需要花费一些时间来理解其背后的概念和实现细节。
不支持传递参数的实例化:静态内部类实现的单例模式无法直接传递参数给单例对象的构造函数,因为静态内部类的实例化是在类加载时完成的。如果需要传递参数的实例化,可能需要使用其他方式实现。
需要额外的类:使用静态内部类实现单例模式需要额外定义一个静态内部类来持有单例对象,这增加了代码中的类数量。虽然这对于代码的组织和结构有一定的好处,但对于一些简单的应用场景来说可能显得有些冗余。
序列化和反序列化的处理:如果单例对象需要支持序列化和反序列化操作,需要额外处理,否则在反序列化时会得到不同的实例。可以通过实现readResolve()
方法来解决这个问题。
????????
? ? ?因为支行的各种软件产品的建设都要报备到省总部,由于历史某种原因,以前报软件产品比较少。所以好多产品都集成一个应用上,一个应用几乎每有新功能需要上线(这种频繁上线方式肯定是不建议的,但有时候又是非常必须的),导致某天第一个客户经理办业务时失败,查原因是实例化和发post请求超时了(内网向外网发请求,网络上做了好多层的代理转发)。小白就想着,那就修改一下外部产品Client的实例化方式吧。
? ? ?痛点:第一个应用加载太慢导致业务办理失败。
? ? ?实现示例代码:
public class EagerSingleton {
private static final SdkClient sdkClient = new SdkClient();
private EagerSingleton() {
// 私有构造函数
}
public static SdkClient getInstance() {
return sdkClient;
}
}
? ? ?在饿汉模式中,单例实例在类加载时就被创建,并且是静态的、final的常量。getInstance() 方法直接返回该实例,而无需进行额外的创建过程。
? ? ?由于在类加载时就创建了单例实例,因此在多线程环境中是线程安全的,不需要额外处理同步问题。
? ? ?优点:
? ? ?缺点:
????????
? ? ?有段时间发现服务器出现频繁的GC,排查发现大量的实例使用比较少但是又不能没有,其中就有外部产品Client对象。最近服务器和网络都做了调优,再加上产品已大量推广使用,在某一些高峰期可会存在大量并发的情况等等种种原因综合考虑。小白就想着,那就修改一下外部产品Client的实例化方式吧。用双重检查锁来实现实例化外部产品Client对象。
? ? ?实现示例代码:
public class DoubleCheckedSingleton {
private volatile static SdkClient sdkClient;
private DoubleCheckedSingleton() {
// 私有构造函数
}
public static SdkClient getInstance() {
if (sdkClient == null) {
synchronized (SdkClient.class) {
if (sdkClient == null) {
sdkClient = new SdkClient();
}
}
}
return sdkClient;
}
}
? ? ?在双重检查锁模式中,通过在getInstance()
方法中使用双重检查来确保只有在实例为null时才创建新的实例。
? ? ?为了确保线程之间的可见性,需要使用volatile关键字修饰instance变量。volatile关键字的作用是禁止指令重排,保证每个线程都能正确看到instance变量的最新值,避免在多线程环境下出现问题。
? ? ?优点:
? ? ?缺点:
????????
? ? ?实现示例代码:
public enum EnumSingleton {
INSTANCE;
private int value;
EnumSingleton() {
// 构造函数可以添加初始化逻辑
value = 10;
}
public int getValue() {
return value;
}
}
? ? ?在这个示例中,EnumSingleton
是一个枚举类,其中INSTANCE
是唯一的枚举常量,代表了单例对象。在构造函数中,我们可以添加额外的初始化逻辑。在这里,我们将value
初始化为10。
? ? ?假设在其他地方调用时,可以通过EnumSingleton.INSTANCE
来获取初始对象,并调用其方法:
int value = EnumSingleton.INSTANCE.getValue();
System.out.println(value); // 输出:10
? ? ?通过枚举类的常量INSTANCE
获取到初始对象,然后可以调用其方法和访问其成员变量。?
? ? ?优点:
Serializable
接口,并且枚举对象的反序列化操作不会创建新的对象。这使得枚举单例在涉及到序列化和反序列化的场景中更加安全。? ? ?缺点:
????????
? ? ?单例模式的本质:控制实例数目。
? ? ?单例模式是用来保证这个类在运行期间只会被创建一个类实例,另外,单例模式还提供了一个全局唯一访间这个类实例的访问点,就是getlnstance 方法。不管采用懒汉式还是饿汉式的实现方式,这 个全局访问点是一样的。
? ? 对于单例模式而言,不管采用何种实现方式,它都是只关心类实例的创建问题,并不关心具体的业务功能。
????????
? ? ?目前Java 里面实现的单例是一个虚拟机的范围。因为装载类的功能是虚拟机的,所以 一个虚拟机在通过自己的ClassLoader 装载饿汉式实现单例
类的时候就会创建一个类的实例。
? ? ?这就意味着如果一个虚拟机里面有很多个ClassLoader,而且这些ClassLoader 都装载某个类的话,就算这个类是单例,它也会产生很多个实例。当然,如果一个机器上有多个虚拟机的话,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有很多个实例,更不会是单例了。
????????
?????????
? ? ?1. 时间与空间
? ? ?2. 线程安全
????????
? ? ?请编写 Triple类,实现最多只能生成3个 Triple类的实例,实例编号分别为 0 , 1 , 2且可以通过 getInstance(int id)来获取该编号对应的实例。