本期内容为自己总结归档,共分6章,本人遇到过的面试问题会重点标记。
(若有任何疑问,可在评论区告诉我,看到就回复)
一、单例模式的核心概念
1.1 单例模式的定义
单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类只有一个实例,并提供一个访问该实例的全局节点。
在UML类图中,单例模式非常简单:
1.2 单例模式的特点
单例模式有三个关键特征:
-
私有构造器 :防止外部直接通过
new创建实例 -
静态实例变量:存储唯一的实例
-
静态获取方法:提供全局访问点
1.3 单例模式 vs 全局变量
你可能会想:为什么不直接使用全局变量?它们之间有什么区别?
| 对比维度 | 单例模式 | 全局变量 |
|---|---|---|
| 延迟初始化 | 支持,只在需要时创建实例 | 程序启动时就初始化 |
| 面向对象 | 是一个完整的对象,可以有方法和状态 | 只是简单的数据 |
| 继承和多态 | 可以继承和实现接口 | 不支持 |
| 访问控制 | 可以控制访问和初始化时机 | 随处可访问,难以控制 |
| 线程安全 | 可以实现线程安全的访问 | 通常不考虑线程安全 |
二、单例模式的实现方式
单例模式的实现有很多种,每种都有其适用场景和优缺点。
2.1 ⭐饿汉式单例:最简单的实现
饿汉式单例在类加载时就创建实例,这是一种急切初始化的方式。
java
/**
* 饿汉式单例
* 优点:实现简单,线程安全
* 缺点:可能造成资源浪费(如果实例从未被使用)
*/
public class EagerSingleton {
// 1. 私有静态实例,类加载时初始化
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 2. 私有构造器,防止外部实例化
private EagerSingleton() {
System.out.println("EagerSingleton 实例被创建");
}
// 3. 公共静态方法,提供全局访问点
public static EagerSingleton getInstance() {
return INSTANCE;
}
// 业务方法
public void doSomething() {
System.out.println("EagerSingleton 执行业务逻辑");
}
}
测试代码:
java
public class SingletonTest {
public static void main(String[] args) {
System.out.println("=== 饿汉式单例测试 ===");
// 多次获取实例,实际是同一个对象
EagerSingleton s1 = EagerSingleton.getInstance();
EagerSingleton s2 = EagerSingleton.getInstance();
System.out.println("s1 == s2: " + (s1 == s2)); // true
System.out.println("s1 hashCode: " + s1.hashCode());
System.out.println("s2 hashCode: " + s2.hashCode());
}
}
优点:
-
实现简单
-
线程安全(JVM保证类加载过程是线程安全的)
缺点:
-
如果实例很大且从未使用,会造成内存浪费
-
不能延迟初始化
2.2 ⭐懒汉式单例:延迟初始化
懒汉式单例在第一次使用时才创建实例,实现了延迟初始化。懒汉的**"懒"**九体现在这
2.2.1 线程不安全的懒汉式
java
/**
* 线程不安全的懒汉式单例
* 优点:延迟初始化,节省资源
* 缺点:多线程环境下不安全
*/
public class UnsafeLazySingleton {
private static UnsafeLazySingleton instance;
private UnsafeLazySingleton() {
System.out.println("UnsafeLazySingleton 实例被创建");
}
public static UnsafeLazySingleton getInstance() {
if (instance == null) {
// 模拟创建实例的耗时操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new UnsafeLazySingleton();
}
return instance;
}
}
多线程测试:
java
public class MultiThreadTest {
public static void main(String[] args) {
System.out.println("=== 线程不安全的懒汉式测试 ===");
// 创建10个线程同时获取实例
for (int i = 0; i < 10; i++) {
new Thread(() -> {
UnsafeLazySingleton instance = UnsafeLazySingleton.getInstance();
System.out.println(
"线程" + Thread.currentThread().getName() +
" 获取实例,hashCode: " + instance.hashCode()
);
}).start();
}
}
}
运行这个测试,你可能会看到多个实例被创建,这证明了线程不安全的问题。
2.2.2 ⭐线程安全的懒汉式(同步方法)
java
/**
* 线程安全的懒汉式单例(同步方法)
* 优点:线程安全,延迟初始化
* 缺点:每次获取实例都要同步,性能较差
*/
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton instance;
private SynchronizedLazySingleton() {
System.out.println("SynchronizedLazySingleton 实例被创建");
}
// 使用synchronized关键字保证线程安全
public static synchronized SynchronizedLazySingleton getInstance() {
if (instance == null) {
instance = new SynchronizedLazySingleton();
}
return instance;
}
}
这种方法虽然线程安全,但每次调用getInstance()都要获取锁,即使实例已经创建。这会带来不必要的性能开销。
2.2.3 双重检查锁定(Double-Checked Locking)
为了解决性能问题,我们可以使用双重检查锁定。
java
/**
* 双重检查锁定单例
* 优点:线程安全,延迟初始化,性能较好
* 缺点:实现较复杂,需要volatile关键字防止指令重排序
*/
public class DoubleCheckedSingleton {
// 使用volatile防止指令重排序
private static volatile DoubleCheckedSingleton instance;
private DoubleCheckedSingleton() {
System.out.println("DoubleCheckedSingleton 实例被创建");
}
public static DoubleCheckedSingleton getInstance() {
// 第一次检查:避免不必要的同步
if (instance == null) {
synchronized (DoubleCheckedSingleton.class) {
// 第二次检查:确保只有一个线程创建实例
if (instance == null) {
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
}
为什么需要volatile?
instance = new DoubleCheckedSingleton();这行代码并不是原子操作,它包含三个步骤:
分配内存空间
初始化对象
将instance指向分配的内存地址
如果没有volatile,JVM可能会进行指令重排序,步骤可能变成1→3→2。这样另一个线程可能拿到一个未完全初始化的对象。
2.3 静态内部类单例:最佳实现之一
java
/**
* 静态内部类单例
* 优点:线程安全,延迟初始化,实现简单
* 缺点:无法传递参数进行初始化
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
System.out.println("StaticInnerClassSingleton 实例被创建");
}
// 静态内部类
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE =
new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
原理:
-
JVM在加载外部类时不会加载内部类
-
只有在调用
getInstance()时才会加载SingletonHolder类 -
类加载过程是线程安全的,由JVM保证
这种方式结合了饿汉式的线程安全和懒汉式的延迟初始化,是目前最推荐的实现方式之一。
2.4 枚举单例
枚举不仅能防止多线程问题,还能防止反射和反序列化破坏单例。
java
/**
* 枚举单例
* 优点:线程安全,防止反射和反序列化攻击,实现简单
* 缺点:不够灵活(不能延迟初始化,不能继承)
*/
public enum EnumSingleton {
INSTANCE;
private EnumSingleton() {
System.out.println("EnumSingleton 实例被创建");
}
public void doSomething() {
System.out.println("枚举单例执行业务逻辑");
}
}
使用示例:
java
public class EnumSingletonTest {
public static void main(String[] args) {
EnumSingleton instance1 = EnumSingleton.INSTANCE;
EnumSingleton instance2 = EnumSingleton.INSTANCE;
System.out.println("instance1 == instance2: " + (instance1 == instance2));
instance1.doSomething();
}
}
三、单例模式的高级话题
3.1 单例模式的序列化与反序列化问题
当单例类实现Serializable接口时,反序列化可能会创建新的实例。会有这个问题:
java
import java.io.*;
/**
* 可序列化的单例(存在问题)
*/
public class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private static SerializableSingleton instance = new SerializableSingleton();
private SerializableSingleton() {
System.out.println("SerializableSingleton 实例被创建");
}
public static SerializableSingleton getInstance() {
return instance;
}
// 测试序列化破坏单例
public static void testSerialization() throws Exception {
SerializableSingleton instance1 = SerializableSingleton.getInstance();
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("singleton.ser")
);
oos.writeObject(instance1);
oos.close();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("singleton.ser")
);
SerializableSingleton instance2 = (SerializableSingleton) ois.readObject();
ois.close();
System.out.println("instance1 == instance2: " + (instance1 == instance2));
System.out.println("instance1 hashCode: " + instance1.hashCode());
System.out.println("instance2 hashCode: " + instance2.hashCode());
}
}
运行上述代码,会发现instance1和instance2不是同一个对象。为了解决这个问题,我们需要添加readResolve()方法:
java
public class SafeSerializableSingleton implements Serializable {
// ... 其他代码同上
// 防止反序列化创建新实例
protected Object readResolve() {
return getInstance();
}
}
3.2 反射攻击与防护
反射可以调用私有构造器,破坏单例模式:
java
import java.lang.reflect.Constructor;
public class ReflectionAttackTest {
public static void main(String[] args) throws Exception {
EagerSingleton instance1 = EagerSingleton.getInstance();
// 使用反射创建新实例
Constructor<EagerSingleton> constructor =
EagerSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true); // 设置可访问私有构造器
EagerSingleton instance2 = constructor.newInstance();
System.out.println("instance1 == instance2: " + (instance1 == instance2));
}
}
防护方法:
java
public class ReflectionSafeSingleton {
private static final ReflectionSafeSingleton INSTANCE =
new ReflectionSafeSingleton();
private static boolean isInstantiated = false;
private ReflectionSafeSingleton() {
synchronized (ReflectionSafeSingleton.class) {
if (isInstantiated) {
throw new RuntimeException("单例已被实例化,禁止通过反射创建");
}
isInstantiated = true;
}
System.out.println("ReflectionSafeSingleton 实例被创建");
}
public static ReflectionSafeSingleton getInstance() {
return INSTANCE;
}
}
3.3 单例模式的扩展:有限多例模式
有时候我们需要的不是严格意义上的单例,而是有限数量的实例,这就是多例模式(Multiton)。
java
import java.util.HashMap;
import java.util.Map;
/**
* 多例模式:创建有限数量的实例
*/
public class Multiton {
// 存储多个实例
private static Map<String, Multiton> instances = new HashMap<>();
// 预定义实例的key
private static final String[] KEYS = {"PRIMARY", "SECONDARY", "TERTIARY"};
static {
// 初始化多个实例
for (String key : KEYS) {
instances.put(key, new Multiton(key));
}
}
private String name;
private Multiton(String name) {
this.name = name;
System.out.println("创建 Multiton 实例: " + name);
}
public static Multiton getInstance(String key) {
return instances.get(key);
}
public String getName() {
return name;
}
}
四、单例模式在Spring框架中的应用
Spring框架虽然没有直接使用传统的单例模式,但它通过IoC容器实现了单例的概念。让我们看看Spring是如何管理单例Bean的。
4.1 Spring的单例作用域
在Spring中,Bean默认就是单例的:
java
@Component
public class ShoppingCartService {
private List<Product> items = new ArrayList<>();
public void addProduct(Product product) {
items.add(product);
}
public List<Product> getItems() {
return items;
}
}
// 在Controller中使用
@RestController
public class ShoppingController {
@Autowired
private ShoppingCartService cartService; // 单例Bean
@PostMapping("/add-to-cart")
public String addToCart(@RequestBody Product product) {
cartService.addProduct(product);
return "添加成功";
}
@GetMapping("/cart-items")
public List<Product> getCartItems() {
return cartService.getItems();
}
}
4.2 Spring单例与线程安全
需要注意的是,Spring的单例是应用上下文级别的单例,不是JVM级别的单例。而且Spring不保证Bean的线程安全,开发者需要自己处理。这里简单举个例子
线程不安全的Spring Bean示例:
java
@Service
public class UnsafeCounterService {
private int count = 0; // 实例变量,存在线程安全问题
public int incrementAndGet() {
return ++count;
}
}
线程安全的Spring Bean示例:
java
@Service
public class SafeCounterService {
// 使用AtomicInteger保证原子性
private AtomicInteger count = new AtomicInteger(0);
public int incrementAndGet() {
return count.incrementAndGet();
}
// 或者使用同步
public synchronized int synchronizedIncrementAndGet() {
return ++count;
}
}
4.3 Spring单例的底层实现
Spring通过DefaultSingletonBeanRegistry类来管理单例Bean。简化版的核心逻辑如下:
java
public class DefaultSingletonBeanRegistry {
// 单例对象的缓存:bean名称 -> bean实例
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 正在创建中的单例对象(用于处理循环依赖)
private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>(16));
public Object getSingleton(String beanName) {
// 1. 先从缓存中获取
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
synchronized (this.singletonObjects) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 2. 如果缓存中没有,开始创建
beforeSingletonCreation(beanName);
try {
// 3. 创建单例对象(实际由BeanFactory实现)
singletonObject = createBean(beanName);
} finally {
afterSingletonCreation(beanName);
}
// 4. 加入缓存
addSingleton(beanName, singletonObject);
}
}
}
return singletonObject;
}
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, singletonObject);
// 从其他缓存中移除
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
}
}
}
五、总结
5.1 何时使用单例模式
应该使用单例模式的场景:
-
需要控制资源访问:如数据库连接池、线程池
-
需要全局状态:如应用的配置信息
-
需要频繁创建和销毁的对象:如日志记录器
-
工具类:如各种工具类(但更推荐静态方法)
注意:
-
Spring等IoC容器提供了更好的单例管理
-
依赖注入比传统单例模式更灵活、更易测试
-
在分布式系统中,需要考虑分布式单例的实现
5.2 实现方式选择
-
如果不考虑内存浪费,饿汉式最简单
-
如果需要延迟初始化,静态内部类是最佳选择
-
如果需要防止反射和序列化攻击,枚举单例最安全
5.3 线程安全
-
单例模式的线程安全需要特别关注
-
双重检查锁定需要配合volatile使用
-
静态内部类和枚举单例天然线程安全