一、单例模式
1、概念
单例模式是一种常用的软件设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式在需要控制实例数目、节省系统资源或确保全局一致性的场景中非常有用。
2、使用场景
配置文件管理器:整个应用只需要一个配置管理器
日志记录器:所有日志应该通过同一个日志对象记录
数据库连接池:管理数据库连接通常只需要一个池
线程池:系统通常只需要一个线程池管理器
二、实现方式
单例模式的实现方式有很多种,最常见的就是"饿汉"和"懒汉"两种模式。
1、饿汉模式
(1)饿汉模式即在类加载的同时创建实例
java
public class EagerSingleton {
// 类加载时就创建实例
private static Singleton instance = new Singleton();
// 私有构造函数防止外部实例化
private Singleton() {}
// 提供全局访问点
public static Singleton getInstance() {
return instance;
}
}
(2)优缺点对比
优点:实现简单,线程安全
缺点:即使不使用也会创建实例,可能浪费资源
(3)为什么线程安全
静态变量的初始化时机:
静态变量 instance 在类加载时由 JVM 完成初始化,JVM 在加载类时会自动加锁,确保一个类只被加载一次,静态变量也只初始化一次。
无竞态条件:
实例的创建发生在类加载时,此时程序尚未进入多线程环境。
调用 getInstance() 时直接返回已初始化的静态变量,无需检查或同步。
2、懒汉模式------单线程版
和饿汉模式相对,懒汉模式是延迟创建实例,第一次使用的时候创建,我们来看代码示例:
(1)代码
java
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
(2)优缺点对比
优点:使用时才创建实例
缺点:每次获取实例都需要同步,性能较低
(3)线程安全问题
通过分析上面的过程,我们可以看出在if条件语句与new实例这一块存在问题(因为这是两个不同的操作,中间可能会被其他线程打断),具体表现:
假设线程A和线程B同时调用getInstance():
线程A执行到检查点(if (instance == null)),发现instance为null
线程B也同时执行到检查点,同样发现instance为null
线程A通过检查,进入创建点,开始创建实例
线程B也通过检查,进入创建点,也开始创建实例
最终会创建两个不同的实例,破坏了单例模式的基本原则
那么针对这样的问题,我们便可以采用加锁的手段进行处理,这样就得出多线程版本。
3、懒汉模式------多线程版本
(1)主要是针对上面线程不安全问题进行加锁处理,代码如下:
java
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
(2)线程安全分析:
getInstance() 被声明为 synchronized,确保同一时间只有一个线程能进入该方法。
线程 A 进入 getInstance(),检查 instance == null,创建实例。
线程 B 必须等待线程 A 释放锁后才能进入,此时 instance 已经初始化,直接返回已有实例。
(3)其他问题分析
对单线程版本加锁解决了线程安全问题,但是每次调用 getInstance() 都要获取锁,即使实例已经创建,仍然会有锁竞争,影响并发性能。
为了解决加锁引入的新问题,我们可以考虑按需加锁,真正涉及到线程安全的时候加锁,不涉及的时候不加锁,即双重检查锁。
(4)双重检查锁代码
java
class Singleton {
private static Singleton instance=null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) { // 同步块
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton(); // 安全初始化
}
}
}
return instance;
}
}
上面代码中两个if条件语句虽然一样,但是他们的作用却不同,第一个if是判断是否需要加锁,如果实例已经创建,就不设计线程安全问题,不需要在加锁了;第二个if是判断是否需要new对象,防止重复实例化。
两个检查协作具体表现是:
1、线程A和线程B同时调用 getInstance()
2、都通过外层检查(此时 instance 确实为 null)
3、线程A先获取锁,进入同步块:
执行内层检查(仍为 null)
创建实例
释放锁
4、线程B获取锁,进入同步块:
执行内层检查(此时 instance 已不为 null)
直接返回已有实例
5、后续所有线程调用时:
外层检查直接返回实例
完全不会进入同步块
我们在仔细分析上面代码,会发现还会有别的问题,那就是主要是指令重排序导致的可见性问题,具体来看:
在Java内存模型中,instance = new Singleton()这行代码实际上包含3个步骤:
1、分配对象内存空间
2、初始化对象(调用构造函数)
3、将instance引用指向该内存地址
由于指令重排序,JVM可能会优化为1→3→2的顺序执行。这样会导致:
线程A执行到步骤3时,instance已不为null,但对象还未初始化
线程B在第一次检查时发现instance不为null,直接返回未初始化完成的对象
那么针对指令重排序这个问题,我们可以使用volatile这个关键字来处理。
(5)改进代码
java
class Singleton {
private static volatile Singleton instance=null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查(无锁)
synchronized (Singleton.class) { // 同步块
if (instance == null) { // 第二次检查(有锁)
instance = new Singleton(); // 安全初始化
}
}
}
return instance;
}
}
volatile的作用
禁止指令重排序:确保对象的初始化顺序为1→2→3
保证可见性:一个线程对instance的修改会立即对其他线程可见
内存屏障:防止读写操作越过屏障,确保操作顺序
通过上面多次的改进,我们解决了线程安全问题和指令重排序问题。
三、总结
单例模式是一种确保类只有一个实例并提供全局访问点的设计模式,适用于需要全局唯一对象的场景如配置管理、日志系统等。主要实现方式包括:饿汉式(类加载时创建实例,简单线程安全但可能浪费资源)、懒汉式(延迟加载但需处理线程安全)、双重检查锁(结合懒加载与线程安全,需volatile防止指令重排序)。选择实现时需权衡线程安全、性能与资源开销问题。