基本概述
单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。
这种模式常用于需要频繁创建和销毁的对象、或者需要协调系统中多个部分的共享资源的等场景
核心特点
1、唯一实例:类只能有一个实例
2、自行创建:类必须自行创建这个实例
3、向整个系统提供这个实例
实现方式
1、饿汉式(线程安全)
饿汉式单例模式是单例模式中最简单也是最直观的一种实现方式。他的核心特点是:在类加载时就立即创建实例,因此成为"饿汉"式(迫不及待的创建实例)
特点
- 类还在的时候就创建实例,而不是在首次使用的时候
- 线程安全:由于在类加载的时候就创建了实例,所以天然线程安全
- 实现简单:逻辑代码清晰,易于理解和维护
- 资源预加载:实例在类加载的时候就占用内存,可能导致资源浪费
基本实现
package gy_basic_project.singleton.demo;
/**
* 饿汉式单例模式 - 基本实现
* 在类加载时就创建实例,线程安全
*/
public class EagerSingleton {
// 在类加载时就创建实例
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 私有构造函数,防止外部创建实例
private EagerSingleton() {
System.out.println("EagerSingleton实例被创建");
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
public void showMessage(String message) {
System.out.println("EagerSingleton: " + message);
}
}
饿汉式的变体:静态代码块实现
package gy_basic_project.singleton.demo;
/**
* 饿汉式单例模式 - 静态代码块实现
* 与基本实现类似,但在静态代码块中创建实例,便于复杂初始化
*/
public class EagerSingletonWithStaticBlock {
private static final EagerSingletonWithStaticBlock INSTANCE;
// 静态代码块中初始化,可以进行复杂的初始化操作
static {
System.out.println("开始初始化EagerSingletonWithStaticBlock...");
INSTANCE = new EagerSingletonWithStaticBlock();
System.out.println("EagerSingletonWithStaticBlock初始化完成");
}
private EagerSingletonWithStaticBlock() {
System.out.println("EagerSingletonWithStaticBlock实例被创建");
}
public static EagerSingletonWithStaticBlock getInstance() {
return INSTANCE;
}
public void showMessage(String message) {
System.out.println("EagerSingletonWithStaticBlock: " + message);
}
public void performInitialization() {
System.out.println("执行一些初始化操作...");
}
}
饿汉式在实际应用中的例子
让我们看一个实际的应用场景,比如数据库连接配置管理器:
package gy_basic_project.singleton.demo;
import java.util.Properties;
/**
* 数据库配置管理器 - 饿汉式实现
* 应用启动时加载配置,供整个应用使用
*/
public class DatabaseConfigManager {
// 饿汉式:在类加载时就初始化配置
private static final DatabaseConfigManager INSTANCE = new DatabaseConfigManager();
// 存储数据库配置
private Properties dbProperties;
private DatabaseConfigManager() {
System.out.println("初始化数据库配置管理器...");
loadConfiguration();
}
private void loadConfiguration() {
dbProperties = new Properties();
// 模拟加载默认配置
dbProperties.setProperty("db.url", "jdbc:mysql://localhost:3306/myapp");
dbProperties.setProperty("db.username", "root");
dbProperties.setProperty("db.password", "password");
dbProperties.setProperty("db.driver", "com.mysql.cj.jdbc.Driver");
dbProperties.setProperty("db.poolSize", "10");
System.out.println("数据库配置加载完成");
}
public static DatabaseConfigManager getInstance() {
return INSTANCE;
}
public String getProperty(String key) {
return dbProperties.getProperty(key);
}
public void setProperty(String key, String value) {
dbProperties.setProperty(key, value);
}
public Properties getAllProperties() {
return (Properties) dbProperties.clone();
}
}
饿汉式使用示例
package gy_basic_project.singleton.demo;
/**
* 饿汉式单例模式演示
*/
public class EagerSingletonDemo {
public static void main(String[] args) {
System.out.println("=== 饿汉式单例模式演示 ===\n");
// 1. 基本饿汉式单例演示
System.out.println("1. 基本饿汉式单例演示:");
System.out.println("首次获取实例:");
EagerSingleton eager1 = EagerSingleton.getInstance();
eager1.showMessage("这是第一个实例调用");
System.out.println("再次获取实例:");
EagerSingleton eager2 = EagerSingleton.getInstance();
eager2.showMessage("这是第二个实例调用");
System.out.println("eager1 == eager2: " + (eager1 == eager2)); // true
System.out.println();
// 2. 静态代码块饿汉式演示
System.out.println("2. 静态代码块饿汉式单例演示:");
EagerSingletonWithStaticBlock staticBlock1 = EagerSingletonWithStaticBlock.getInstance();
staticBlock1.performInitialization();
staticBlock1.showMessage("这是静态代码块饿汉式实例");
EagerSingletonWithStaticBlock staticBlock2 = EagerSingletonWithStaticBlock.getInstance();
System.out.println("staticBlock1 == staticBlock2: " + (staticBlock1 == staticBlock2)); // true
System.out.println();
// 3. 数据库配置管理器演示
System.out.println("3. 数据库配置管理器演示:");
DatabaseConfigManager config1 = DatabaseConfigManager.getInstance();
System.out.println("数据库URL: " + config1.getProperty("db.url"));
System.out.println("用户名: " + config1.getProperty("db.username"));
// 修改配置
config1.setProperty("db.poolSize", "20");
// 获取同一实例,验证配置已更新
DatabaseConfigManager config2 = DatabaseConfigManager.getInstance();
System.out.println("更新后的连接池大小: " + config2.getProperty("db.poolSize"));
System.out.println("config1 == config2: " + (config1 == config2)); // true
System.out.println();
// 4. 多线程安全性演示
System.out.println("4. 多线程安全性演示:");
Thread[] threads = new Thread[5];
EagerSingleton[] instances = new EagerSingleton[5];
for (int i = 0; i < 5; i++) {
final int index = i;
threads[i] = new Thread(() -> {
instances[index] = EagerSingleton.getInstance();
System.out.println("线程 " + Thread.currentThread().getName() +
" 获取实例: " + instances[index].hashCode());
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// 验证实例相同
boolean allSame = true;
int baseHash = instances[0].hashCode();
for (int i = 1; i < instances.length; i++) {
if (instances[i].hashCode() != baseHash) {
allSame = false;
break;
}
}
System.out.println("所有线程获取的实例相同: " + allSame);
System.out.println("\n=== 饿汉式单例演示完成 ===");
}
}
饿汉式的优缺点
优点
- 线程安全:由于在类记载时就创建实例,不需要额外的同步措施
- 实现简单:代码逻辑清晰,易于理解和维护
- 性能好:获取实例的时候无需额外操作,性能优秀
- 类加载安全:利用类加载机制保证实例唯一性
缺点:
- 资源浪费:即使不使用实例也会占用内存
- 无法延迟加载:不能按需创建实例
- 初始化时机不可控:在类加载时就创建,可能在应用启动早期就占用资源
适用场景
- 实例占用资源不多的
- 初始化较快
- 程序启动时必然会使用该实例
- 多线程安全较高的场景
2、懒汉式(线程不安全)
懒汉式(非线程安全)是单例模式的一种实现方式,其特点是只在需要时才创建实例,体现了"懒加载"的思想
特点
- 延迟加载:只有在首次调用getInstance()方法的时候才创建实例
- 节省资源:如果实例从未被使用,就不会被创建,节省内存
- 非线程安全:在多线程环境下可能出现多个实例的问题
- 实现简单:代码逻辑清晰,易于理解
基本实现
package gy_basic_project.singleton.demo;
/**
* 懒汉式单例模式(非线程安全)
* 在第一次调用getInstance()时才创建实例
*/
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
System.out.println("LazySingleton实例被创建");
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
public void showMessage(String message) {
System.out.println("LazySingleton: " + message);
}
}
为什么会线程不安全?
再多线程环境下,懒汉式会出现竞态条件(Race Condition)
1、线程A检查instance是否为null,结果为true;
2、此时线程B也检查instance是否为null,结果也是true
3、线程A继续执行,创建实例
4、线程B也继续执行,创建另一个实例
5、最终导致创建了多个实例
package gy_basic_project.singleton.demo;
/**
* 懒汉式单例模式演示
*/
public class LazySingletonDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 懒汉式单例模式演示 ===\n");
// 1. 基本使用演示
System.out.println("1. 基本使用演示:");
System.out.println("首次获取实例:");
LazySingleton lazy1 = LazySingleton.getInstance();
lazy1.showMessage("这是第一个实例");
System.out.println("再次获取实例:");
LazySingleton lazy2 = LazySingleton.getInstance();
lazy2.showMessage("这是第二个实例");
System.out.println("lazy1 == lazy2: " + (lazy1 == lazy2)); // true(在单线程下)
System.out.println();
// 2. 延迟加载演示 - 实例在首次调用时才创建
System.out.println("2. 延迟加载演示:");
System.out.println("声明前,实例尚未创建");
// 只有在调用getInstance()时才创建实例
LazySingleton lazy3 = LazySingleton.getInstance();
System.out.println("实例创建后: " + (lazy1 == lazy3)); // true
System.out.println();
// 3. 线程安全问题演示
System.out.println("3. 线程安全问题演示:");
demonstrateThreadSafetyIssue();
System.out.println("\n=== 懒汉式单例演示完成 ===");
}
/**
* 演示懒汉式在多线程环境下的问题
*/
private static void demonstrateThreadSafetyIssue() throws InterruptedException {
// 重置实例(仅用于演示)
resetLazySingleton();
// 创建多个线程同时获取实例
Thread[] threads = new Thread[10];
LazySingleton[] instances = new LazySingleton[10];
System.out.println("创建10个线程同时获取实例...");
for (int i = 0; i < 10; i++) {
final int index = i;
threads[i] = new Thread(() -> {
// 添加短暂延时,增加竞争条件发生的可能性
try {
Thread.sleep((long) (Math.random() * 10));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
instances[index] = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() +
" 获取实例,哈希码: " + instances[index].hashCode());
}, "Thread-" + i);
}
// 启动所有线程
for (Thread thread : threads) {
thread.start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
// 检查是否所有线程获得了相同的实例
System.out.println("\n检查实例唯一性:");
boolean allSame = true;
LazySingleton firstInstance = instances[0];
for (int i = 1; i < instances.length; i++) {
if (instances[i] != firstInstance) {
allSame = false;
System.out.println("发现不同实例!线程" + i + "获得不同实例");
}
}
System.out.println("所有线程获得相同实例: " + allSame);
if (!allSame) {
System.out.println("检测到线程安全问题:创建了多个实例!");
} else {
System.out.println("未观察到线程安全问题(可能是由于执行速度太快,没有发生竞争)");
}
}
/**
* 重置LazySingleton实例(仅用于演示目的)
*/
@SuppressWarnings("unchecked")
private static void resetLazySingleton() {
try {
java.lang.reflect.Field instanceField = LazySingleton.class.getDeclaredField("instance");
instanceField.setAccessible(true);
instanceField.set(null, null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
改进版本:带延迟的懒汉式(更容易重现问题)
package gy_basic_project.singleton.demo;
/**
* 懒汉式单例模式(非线程安全)- 用于演示问题的版本
* 在创建实例前添加延迟,更容易观察到线程安全问题
*/
public class ProblematicLazySingleton {
private static ProblematicLazySingleton instance;
private ProblematicLazySingleton() {
// 模拟构造函数执行时间
try {
Thread.sleep(1); // 添加延迟,更容易产生竞态条件
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("ProblematicLazySingleton实例被创建,线程: " +
Thread.currentThread().getName());
}
public static ProblematicLazySingleton getInstance() {
if (instance == null) {
// 在这里添加延迟,让竞态条件更容易发生
try {
Thread.sleep(1); // 增加竞态条件发生的概率
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
instance = new ProblematicLazySingleton();
}
return instance;
}
public void showMessage(String message) {
System.out.println("ProblematicLazySingleton: " + message +
" (实例哈希码: " + this.hashCode() + ")");
}
}
懒汉式的优缺点
优点
- 延迟加载:只在需要时才创建实例,节省内存
- 实现简单:代码逻辑清晰,易于理解
- 按需创建:如果实例从未使用,则不会创建
缺点
- 线程不安全:在多线程环境下可能创建多个实例
- 不适合高并发:在高并发场景下会导致问题
多线程高并发环境下,可以是用同步锁或者双重检查锁定
3、懒汉式(线程安全)
在懒汉式(非线程安全)的基础上实现,默认的实现方法是同步方法的方式,后续通过变体分别演变为"双重检查锁定""静态内部类"
当前我们先讲解同步方法版本懒汉式
懒汉式--同步方法方式(线程安全但性能较低)
只是在获取实例的方法上添加同步锁用来控制多线程环境下的安全
只适用于对于性能要求不高的程序中使用
package gy_basic_project.singleton.demo;
/**
* 懒汉式单例模式 - 线程安全版本(同步方法)
* 通过synchronized关键字保证线程安全,但性能较低
*/
public class ThreadSafeLazySingleton {
private static ThreadSafeLazySingleton instance;
private ThreadSafeLazySingleton() {
System.out.println("ThreadSafeLazySingleton实例被创建,线程: " +
Thread.currentThread().getName());
}
/**
* 使用synchronized关键字确保线程安全
* 但每次调用都会进行同步,影响性能
*/
public static synchronized ThreadSafeLazySingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeLazySingleton();
}
return instance;
}
public void showMessage(String message) {
System.out.println("ThreadSafeLazySingleton: " + message);
}
}
4、双重检查锁定(推荐)
双重检查锁定(Duble-Checked Loking)是一种优化的单例模式实现方式,他在保持懒加载特性的同时确保线程的安全,同时避免了每次访问需要同步的性能开销
基本实现
package gy_basic_project.singleton.demo;
/**
* 双重检查锁定单例模式
* 结合了懒加载和线程安全的特性,同时避免了每次访问都要同步
*/
public class DoubleCheckedLockingSingleton {
/**
* 使用volatile关键字的重要性:
* 1. 确保变量的可见性:一个线程修改后,其他线程能立即看到变化
* 2. 防止指令重排序:确保对象完全初始化后再赋值给instance引用
*/
private static volatile DoubleCheckedLockingSingleton instance;
private DoubleCheckedLockingSingleton() {
System.out.println("DoubleCheckedLockingSingleton实例被创建,线程: " +
Thread.currentThread().getName());
}
public static DoubleCheckedLockingSingleton getInstance() {
// 第一次检查:无锁检查,提高性能
if (instance == null) {
// 进入同步块,确保只创建一个实例
synchronized (DoubleCheckedLockingSingleton.class) {
// 第二次检查:确保只创建一个实例
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
}
}
return instance;
}
public void showMessage(String message) {
System.out.println("DoubleCheckedLockingSingleton: " + message);
}
}
双重检查锁定的工作流程
1、第一次检查(无锁)
-
如果实例已经存在,直接返回避免不必要的同步开销
-
提高了获取实例的性能
if (instance == null) {
// 如果instance不为null,直接返回,无需进入同步块
}
2、同步块
-
当时李不存在的时候进入同步块
-
防止多个线程同时创建实例
synchronized (DoubleCheckedLockingSingleton.class) {
// 确保只创建一个实例
}
3、第二次检查(同步块内)
-
在同步块内再次检查,确保只创建一个实例
-
防止在等待锁的过程中,其他线程已经创建了实例
if (instance == null) {
instance = new DoubleCheckedLockingSingleton();
}
为什么需要volatile关键字?
无volatile时可能出现的问题
- 线程A创建实例后,由于CPU缓存机制线程B可能看不到线程A的修改,继续创建新实例
- 指令重排问题
-
- 在JVM中,new操作可能被分解为以下步骤
-
-
- 1、分配内存
- 初始化对象
- 将instance指向被分配的内存地址
-
-
- 如果发生重排序变成了1->3->2:
-
-
- instance指向了内存地址(不为null)
- 但对象尚未初始化完成
- 其他线程可能获取到未完全化的对象
-
5、静态内部类(推荐)
特点
- 延迟加载:实例在第一次调用getInstance()方法的时候才会被调用
- 线程安全:利用JVM类加载机制保证线程安全,无需额外同步
- 高性能:避免同步带来的性能开销
基本实现
public class Singleton {
// 私有构造函数,防止外部实例化
private Singleton() {}
// 静态内部类,持有单例实例
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
// 提供全局访问点
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
注意事项
- 私有构造函数:必须为私有,防止外部通过new创建实例
- 序列化问题:若类实现了Serializable接口,需要重写readResolve()方法防止反序列化破坏单例
- 反射攻击:可通过在构造函数中添加逻辑判断防御反射创建多个实例
优缺点
优点:
- 线程安全且无性能损耗
- 实现简单逻辑清晰
- 支持延迟加载
缺点:
- 不支持带参的构造函数
- 对反射和序列化的防护需要额外处理
适用场景
- 需要延迟加载的单例对象
- 多线程环境下的高性能单例需求
- 不涉及复杂初始化逻辑的场景
6、枚举实现(最安全)
特点
- 天然线程安全:枚举的实例化由JVM保证线程安全
- 防止反射攻击:枚举类型无法通过反射创建新实例
- 防止序列化破坏:枚举类型默认实现序列化机制,反序列化不会创建新对象
- 简洁高效:代码量少,语义清晰
基本实现
public enum Singleton {
INSTANCE;
public void doSomething() {
// 业务逻辑
}
}
注意事项
- 功能限制:枚举类型无法继承其他类(因为已隐式继承Enum),但可实现接口
- 初始化参数:若需要传递参数,可在枚举构造函数中处理,但需要注意参数的不可变性
- 序列化兼容:虽然枚举天然支持序列化,但仍需要确保业务逻辑与序列化行为一致
优缺点
优点:
- 最安全的单例实现方式
- 自动处理线程安全和序列化问题
- 代码简介,易于维护
缺点:
- 不支持延迟加载(枚举类型实例在类加载时即创建)
- 功能受限,无法继承其他类
适用场景
- 需要绝对安全的单例实现
- 无延迟加载需求的场景
- 对代码简洁性和可维护性要求较高的项目
单例模式的优缺点
优点
1、内存节省:只存在一个实例减少内存开销
2、性能提升:避免重复创建和销毁对象
3、全局访问:提供了统一的访问点
4、严格控制:严格控制实例的数量
缺点
1、扩展困难:单例类不易扩展
2、测试困难:难以进行单元测试
3、职责过重:吃蛋过多的职责,违背单一职责原则
4、潜在耦合:可能增加模块间的耦合度
应用场景
- 数据库连接池
- 日志记录器
- 缓存管理器
- 配置管理器
- 线程池
在java中,静态内部类和双重检查锁定是比较常见的实现方式,他们既保证了线程安全,又避免同步带来的性能影响。而枚举实现则是最安全的实现方式,可以防止反射攻击和序列化破坏单例;