一、什么是单例模式?
单例模式是一种常用的设计模式,目的就是确保某个类在程序中只有一个实例,并且提供一个全局访问入口。通过这个模式,我们能够保证全局共享同一个对象实例,避免了多次实例化同一个对象,节省内存,提升性能。
二、单例模式的优点
-
节省内存与计算资源
单例模式确保只会创建一个对象实例,避免了多次创建同一个对象,减少了内存和计算资源的消耗。
-
方便管理与控制
对象的管理变得更加集中,能够方便地对单例对象进行控制与管理,特别适用于全局共享的资源,比如数据库连接池、日志记录器等。
-
线程安全
单例模式常常与多线程环境配合使用,确保多线程情况下只能有一个实例创建,避免了线程安全问题。
三、线程安全的单例模式实现
在多线程环境下,我们通常需要保证单例实例的创建是线程安全的。下面是使用"双重检查锁"实现线程安全的单例模式代码:
public class Singleton {
private static volatile Singleton singleton;
// 私有构造函数,防止外部通过new来创建实例
private Singleton() {
}
// 获取单例对象的方法
public static Singleton getInstance() {
// 第一层检查,避免不必要的同步
if (singleton == null) {
synchronized (Singleton.class) {
// 第二层检查,确保只有第一个线程创建实例
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
四、为什么需要"双重检查锁模式"?
在上述代码中,我们使用了双重检查锁(Double-Checked Locking)来确保线程安全并优化性能。理解为什么需要两次 if
判断,首先要了解以下几个方面:
1. 第一个 if
判断
- 目的是避免不必要的同步 。当第一个线程进入时,实例还未创建,它会进入
synchronized
块去创建实例。而如果实例已经被创建,后续的线程就无需进入同步块,直接返回已有实例,从而提高性能。
2. 第二个 if
判断
- 保证单例唯一性 。假设有两个线程 A 和 B 都同时执行了第一个
if
判断,并且都发现singleton == null
。此时,线程 A 获得了锁并创建了实例,而线程 B 在等待锁的过程中,也会经过第一次if
判断,并进入同步块,但此时线程 A 已经创建了实例。 - 如果没有第二个
if
判断,线程 B 也会重新创建一个新的实例,这就破坏了单例模式的初衷。
五、为什么需要 volatile
关键字?
在这段代码中,我们给 singleton
加上了 volatile
关键字。volatile
在这里的作用是保证线程之间的可见性,防止指令重排序带来的问题。
1. singleton = new Singleton()
不是原子操作
在 JVM 中,执行 singleton = new Singleton()
至少包含以下三步:
- 为
singleton
分配内存空间 - 调用
Singleton
的构造函数初始化对象 - 将
singleton
引用指向分配的内存空间
在多线程环境下,JVM 可能会对这三步指令进行重排序。假如指令重排序导致 singleton
被赋值为空(第3步)但对象还未初始化完成(第2步未完成),这会导致其他线程误以为 singleton
已经初始化完成,从而访问一个未完全初始化的对象。
2. volatile
保证可见性
volatile
可以避免指令重排序,从而确保在一个线程中对 singleton
的修改对其他线程立即可见,确保单例实例只会被初始化一次。
六、总结
单例模式在保证程序中对象的唯一性和全局访问的同时,能够有效节省资源、提高效率。在多线程环境下,我们通过"双重检查锁模式"来确保单例的线程安全,并通过 volatile
关键字保证对象的可见性和防止指令重排序。
通过以上的学习,你应该已经理解了如何优雅地实现一个线程安全的单例模式。这个实现方式不仅性能优越,而且能够在多线程环境中有效防止并发问题,是许多高并发系统中不可或缺的设计模式之一。