《设计模式》第二篇:单例模式

本期内容为自己总结归档,共分6章,本人遇到过的面试问题会重点标记。

《设计模式》第一篇:初识

《设计模式》第二篇:单例模式

《设计模式》第三篇:工厂模式

《设计模式》第四篇:观察模式

《设计模式》第五篇:策略模式

《设计模式》第六篇:装饰器模式

《设计模式》第七篇:适配器模式

《设计模式》第八篇:创建型模式

《设计模式》第九篇:结构型模式

《设计模式》第十篇:行为型模式

《设计模式》第十一篇:总结&常用案例

(若有任何疑问,可在评论区告诉我,看到就回复)

一、单例模式的核心概念

1.1 单例模式的定义

单例模式(Singleton Pattern)是一种创建型设计模式,它保证一个类只有一个实例,并提供一个访问该实例的全局节点。

在UML类图中,单例模式非常简单:

1.2 单例模式的特点

单例模式有三个关键特征:

  1. 私有构造器 :防止外部直接通过new创建实例

  2. 静态实例变量:存储唯一的实例

  3. 静态获取方法:提供全局访问点

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();这行代码并不是原子操作,它包含三个步骤:

  1. 分配内存空间

  2. 初始化对象

  3. 将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());
    }
}

运行上述代码,会发现instance1instance2不是同一个对象。为了解决这个问题,我们需要添加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 何时使用单例模式

应该使用单例模式的场景:

  1. 需要控制资源访问:如数据库连接池、线程池

  2. 需要全局状态:如应用的配置信息

  3. 需要频繁创建和销毁的对象:如日志记录器

  4. 工具类:如各种工具类(但更推荐静态方法)

注意:

  • Spring等IoC容器提供了更好的单例管理

  • 依赖注入比传统单例模式更灵活、更易测试

  • 在分布式系统中,需要考虑分布式单例的实现

5.2 实现方式选择

  • 如果不考虑内存浪费,饿汉式最简单

  • 如果需要延迟初始化,静态内部类是最佳选择

  • 如果需要防止反射和序列化攻击,枚举单例最安全

5.3 线程安全

  • 单例模式的线程安全需要特别关注

  • 双重检查锁定需要配合volatile使用

  • 静态内部类和枚举单例天然线程安全

相关推荐
你才是臭弟弟3 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
码农水水3 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
summer_du3 小时前
IDEA插件下载缓慢,如何解决?
java·ide·intellij-idea
东东5163 小时前
高校智能排课系统 (ssm+vue)
java·开发语言
余瑜鱼鱼鱼3 小时前
HashTable, HashMap, ConcurrentHashMap 之间的区别
java·开发语言
SunnyDays10113 小时前
使用 Java 自动设置 PDF 文档属性
java·pdf文档属性
J_liaty4 小时前
23种设计模式一抽象工厂模式‌
设计模式·抽象工厂模式
我是咸鱼不闲呀4 小时前
力扣Hot100系列16(Java)——[堆]总结()
java·算法·leetcode
what丶k4 小时前
SpringBoot3 配置文件使用全解析:从基础到实战,解锁灵活配置新姿势
java·数据库·spring boot·spring·spring cloud