创建型设计模式深度解析
面试官:单例模式你了解吗?有几种写法?
你:有多种写法,常用的有饿汉式、懒汉式、双检锁(DCL)、静态内部类和枚举。
面试官:DCL 双检锁里,volatile 为什么不能去掉?
这是单例模式面试的终极追问。能解释清楚指令重排序问题的候选人,才能证明真正理解了并发编程的底层原理。
链式追问一:单例模式
Section titled “链式追问一:单例模式”Q1:单例模式有几种写法?各自的优缺点是什么?必考
Section titled “Q1:单例模式有几种写法?各自的优缺点是什么?”核心要求:
- 私有构造器(防止外部 new)
- 静态实例变量
- 公共静态获取方法
五种实现方式对比:
| 实现方式 | 线程安全 | 懒加载 | 防止反射破坏 | 防止序列化破坏 | 推荐度 |
|---|---|---|---|---|---|
| 饿汉式 | ✅ | ❌ | ❌ | ❌ | ⭐⭐⭐ |
| 懒汉式(同步方法) | ✅ | ✅ | ❌ | ❌ | ⭐ |
| DCL 双检锁 | ✅ | ✅ | ❌ | ❌ | ⭐⭐⭐ |
| 静态内部类 | ✅ | ✅ | ❌ | ❌ | ⭐⭐⭐⭐ |
| 枚举 | ✅ | ❌ | ✅ | ✅ | ⭐⭐⭐⭐⭐ |
实现代码详解:
// ==================== 1. 饿汉式 ====================public class Singleton1 { // 类加载时就创建,JVM 保证线程安全 private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() { return INSTANCE; }}// 优点:简单,线程安全// 缺点:类加载就创建,可能浪费内存(实际很少成为问题)
// ==================== 2. 懒汉式(线程不安全) ====================public class Singleton2 { private static Singleton2 instance;
private Singleton2() {}
// ❌ 线程不安全!多线程可能创建多个实例 public static Singleton2 getInstance() { if (instance == null) { instance = new Singleton2(); } return instance; }}
// ==================== 3. DCL 双检锁 ====================public class Singleton3 { // volatile 不可省略!防止指令重排序 private static volatile Singleton3 instance;
private Singleton3() {}
public static Singleton3 getInstance() { if (instance == null) { // 第一次检查:避免不必要的锁竞争 synchronized (Singleton3.class) { if (instance == null) { // 第二次检查:防止重复创建 instance = new Singleton3(); } } } return instance; }}// 优点:懒加载 + 线程安全 + 高性能(锁粒度小)// 缺点:volatile 有性能开销(现代 JVM 很小)
// ==================== 4. 静态内部类 ====================public class Singleton4 { private Singleton4() {}
// 静态内部类不会在 Singleton4 加载时初始化 private static class Holder { private static final Singleton4 INSTANCE = new Singleton4(); }
public static Singleton4 getInstance() { return Holder.INSTANCE; // 第一次调用时才加载 Holder }}// 优点:懒加载 + 线程安全(JVM 保证)+ 无锁// 缺点:无法传参初始化
// ==================== 5. 枚举(最佳实践) ====================public enum Singleton5 { INSTANCE;
public void doSomething() { }}// 调用:Singleton5.INSTANCE.doSomething()// 优点:线程安全 + 防止反射 + 防止序列化// 缺点:无法懒加载Q2:DCL 中 volatile 为什么不能去掉?必考
Section titled “Q2:DCL 中 volatile 为什么不能去掉?”核心原因:指令重排序导致半初始化状态
对象创建的三个步骤:
instance = new Singleton() 在字节码层面分为三步:
步骤 1: memory = allocate() // 分配内存空间步骤 2: ctorInstance(memory) // 初始化对象(调用构造方法)步骤 3: instance = memory // 将引用指向内存地址
问题:步骤 2 和 3 可能重排序为 1 → 3 → 2重排序导致的问题:
┌────────────────────────────────────────────────┐│ 无 volatile 时的并发问题 │├────────────────────────────────────────────────┤│ ││ 线程 A 线程 B ││ │ │ ││ ├─ 1. 分配内存 │ ││ │ │ ││ ├─ 3. instance = memory ──────┼─ 第一次检查 ││ │ (instance != null) │ instance ││ │ │ != null ││ │ │ ↓ ││ │ │ 返回实例 ││ │ │ (使用半 ││ │ │ 初始化对象 ││ ├─ 2. 初始化对象 │ → 错误!) ││ │ │ ││ ││ 线程 B 使用了未初始化完成的对象! ││ │└────────────────────────────────────────────────┘volatile 的作用:
// volatile 通过内存屏障禁止指令重排序private static volatile Singleton instance;
// 编译后插入内存屏障:// 1. 分配内存// 2. 初始化对象// StoreStore 屏障(禁止上方指令重排到下方)// 3. instance = memory// StoreLoad 屏障(禁止下方指令重排到上方)
// 保证:instance 非空时,对象一定已初始化完成性能影响:
| 操作 | 耗时(纳秒) | 说明 |
|---|---|---|
| 普通读 | 1 | 无屏障 |
| volatile 读 | 10 | LoadLoad + LoadStore 屏障 |
| 普通写 | 1 | 无屏障 |
| volatile 写 | 20 | StoreStore + StoreLoad 屏障 |
Q3:如何防止反射和序列化破坏单例?高频
Section titled “Q3:如何防止反射和序列化破坏单例?”反射破坏单例:
// 反射可以破坏私有构造器Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();constructor.setAccessible(true); // 暴力反射Singleton instance1 = constructor.newInstance();Singleton instance2 = constructor.newInstance();System.out.println(instance1 == instance2); // false!破坏了单例
// 防止方法:构造器中检查private Singleton() { if (Holder.INSTANCE != null) { throw new RuntimeException("单例已存在,禁止反射创建"); }}序列化破坏单例:
// 序列化 + 反序列化会创建新对象Singleton instance1 = Singleton.getInstance();ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(instance1);
ObjectInputStream ois = new ObjectInputStream( new ByteArrayInputStream(bos.toByteArray()));Singleton instance2 = (Singleton) ois.readObject();
System.out.println(instance1 == instance2); // false!破坏了单例防止方法对比:
| 方法 | 防止反射 | 防止序列化 | 实现复杂度 |
|---|---|---|---|
| 构造器检查 | ✅ | ❌ | 低 |
| readResolve 方法 | ❌ | ✅ | 低 |
| 枚举 | ✅ | ✅ | 极低 |
// 方法 1:readResolve 防止序列化破坏public class Singleton implements Serializable { private static final long serialVersionUID = 1L;
// 反序列化时,JVM 会调用此方法返回指定对象 private Object readResolve() { return getInstance(); // 返回单例实例,而非创建新对象 }}
// 方法 2:枚举(推荐)public enum Singleton { INSTANCE;}// JDK 保证:枚举的反射会抛异常,序列化/反序列化返回同一实例枚举防止反射的原理:
// Constructor.newInstance 源码片段if ((clazz.getModifiers() & Modifier.ENUM) != 0) { throw new IllegalArgumentException("Cannot reflectively create enum objects");}// JDK 在构造器层面禁止了枚举的反射创建Q4:单例模式在 Spring 中的应用?高频
Section titled “Q4:单例模式在 Spring 中的应用?”Spring Bean 的单例实现:
// Spring 的单例是"容器级单例",不是 JVM 级单例public class DefaultSingletonBeanRegistry { // 单例缓存:Map 存储 private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 获取单例 Bean public Object getSingleton(String beanName) { Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { synchronized (this.singletonObjects) { singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = createBean(beanName); // 创建 Bean this.singletonObjects.put(beanName, singletonObject); } } } return singletonObject; }}Spring 单例 vs 传统单例:
| 对比维度 | 传统单例 | Spring 单例 |
|---|---|---|
| 作用范围 | JVM 级别(ClassLoader) | 容器级别(ApplicationContext) |
| 实现方式 | 静态变量 + 私有构造器 | Map 缓存 + 依赖注入 |
| 多实例 | 不支持 | 支持(prototype、request 等 scope) |
| 生命周期管理 | 无 | 有(初始化、销毁回调) |
┌────────────────────────────────────────────────┐│ Spring 单例的作用域 │├────────────────────────────────────────────────┤│ ││ JVM ││ ├── ClassLoader 1 ││ │ └── ApplicationContext A ││ │ └── Singleton Bean X ││ │ ││ └── ClassLoader 2 ││ └── ApplicationContext B ││ └── Singleton Bean X ││ ││ 结论: ││ - 一个 ApplicationContext 中,Bean X 是单例 ││ - 多个 ApplicationContext,可能有多个实例 ││ - 传统单例是 ClassLoader 级别 ││ │└────────────────────────────────────────────────┘链式追问二:工厂模式
Section titled “链式追问二:工厂模式”Q5:简单工厂、工厂方法、抽象工厂的区别?必考
Section titled “Q5:简单工厂、工厂方法、抽象工厂的区别?”三种工厂模式对比:
┌────────────────────────────────────────────────┐│ 工厂模式演进路径 │├────────────────────────────────────────────────┤│ ││ 简单工厂 ││ └── 一个工厂,switch 分发产品 ││ └── 问题:新增产品要修改工厂(违反OCP) ││ ││ 工厂方法 ││ └── 工厂接口 + 多个工厂实现 ││ └── 一个工厂只生产一种产品 ││ └── 符合 OCP,但类数量爆炸 ││ ││ 抽象工厂 ││ └── 工厂接口 + 多个工厂实现 ││ └── 一个工厂生产一族产品 ││ └── 解决产品族问题 ││ │└────────────────────────────────────────────────┘代码实现对比:
// ==================== 1. 简单工厂 ====================public class SimpleFactory { public static Product create(String type) { switch (type) { case "A": return new ProductA(); case "B": return new ProductB(); default: throw new IllegalArgumentException("未知产品类型"); } }}// 问题:新增产品必须修改工厂类(违反开闭原则)
// ==================== 2. 工厂方法 ====================public interface ProductFactory { Product create(); // 每个工厂只生产一种产品}
public class ProductAFactory implements ProductFactory { @Override public Product create() { return new ProductA(); }}
public class ProductBFactory implements ProductFactory { @Override public Product create() { return new ProductB(); }}// 新增产品:新增工厂类,不修改已有代码(符合 OCP)
// ==================== 3. 抽象工厂(产品族) ====================// 场景:跨平台 UI 组件(Windows/Mac 风格)
public interface UIFactory { Button createButton(); // 产品1:按钮 Dialog createDialog(); // 产品2:对话框}
public class WindowsUIFactory implements UIFactory { @Override public Button createButton() { return new WindowsButton(); // Windows 风格按钮 }
@Override public Dialog createDialog() { return new WindowsDialog(); // Windows 风格对话框 }}
public class MacUIFactory implements UIFactory { @Override public Button createButton() { return new MacButton(); // Mac 风格按钮 }
@Override public Dialog createDialog() { return new MacDialog(); // Mac 风格对话框 }}产品族示意图:
┌────────────────────────────────────────────────┐│ 抽象工厂:产品族概念 │├────────────────────────────────────────────────┤│ ││ UIFactory(抽象工厂) ││ │ ││ ┌─────────┴─────────┐ ││ │ │ ││ WindowsUIFactory MacUIFactory ││ │ │ │ │ ││ │ │ │ │ ││ Button Dialog Button Dialog ││ │ │ │ │ ││ Windows Windows Mac Mac ││ Button Dialog Button Dialog ││ ││ 产品族:Windows 风格 / Mac 风格 ││ 产品等级:Button / Dialog ││ │└────────────────────────────────────────────────┘Q6:Spring 中有哪些工厂模式的应用?高频
Section titled “Q6:Spring 中有哪些工厂模式的应用?”Spring 的三大工厂:
// ==================== 1. BeanFactory(工厂方法) ====================public interface BeanFactory { Object getBean(String name) throws BeansException; <T> T getBean(Class<T> requiredType) throws BeansException; // Spring 容器的顶层接口,生产 Bean}
// 实现类public class DefaultListableBeanFactory implements BeanFactory { // 生产 Bean 的逻辑}
// ==================== 2. FactoryBean(自定义工厂) ====================public interface FactoryBean<T> { T getObject() throws Exception; // 生产对象 Class<?> getObjectType(); // 对象类型 default boolean isSingleton() { // 是否单例 return true; }}
// 自定义 FactoryBean@Componentpublic class SqlSessionFactoryBean implements FactoryBean<SqlSession> { @Override public SqlSession getObject() { // 自定义创建逻辑 return new SqlSession(configuration); }
@Override public Class<?> getObjectType() { return SqlSession.class; }}
// ==================== 3. ApplicationContext(增强工厂) ====================public interface ApplicationContext extends BeanFactory { // 在 BeanFactory 基础上增加了: // - 国际化支持 // - 事件发布 // - 资源加载 // - AOP 支持}FactoryBean vs BeanFactory:
| 对比维度 | BeanFactory | FactoryBean |
|---|---|---|
| 作用 | Spring 容器的顶层接口 | 用户自定义 Bean 创建逻辑 |
| 谁来用 | 框架 | 开发者 |
| 生产方式 | 反射 + 依赖注入 | 用户自定义 |
| 典型应用 | 所有 Bean 的生产 | MyBatis SqlSessionFactory |
MyBatis 的 FactoryBean 应用:
// MyBatis 整合 Spring 的核心public class SqlSessionFactoryBean implements FactoryBean<SqlSession>, InitializingBean { private DataSource dataSource; private Configuration configuration;
@Override public SqlSession getObject() throws Exception { SqlSessionFactory factory = new SqlSessionFactoryBuilder() .build(configuration); return factory.openSession(); }
@Override public Class<?> getObjectType() { return SqlSession.class; }}
// Spring 配置@Beanpublic SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean;}链式追问三:建造者模式
Section titled “链式追问三:建造者模式”Q7:建造者模式解决什么问题?什么时候用?高频
Section titled “Q7:建造者模式解决什么问题?什么时候用?”适用场景:
- 构建参数很多的复杂对象
- 参数有必填和可选之分
- 需要分步骤构建
- 构建过程需要校验
问题:望远镜构造函数:
// ❌ 问题:参数过多,难以理解Pizza pizza = new Pizza("large", "thin", true, false, true, "tomato", "mozzarella");// ↑ ↑ ↑ ↑ ↑ ↑ ↑// size crust cheese pepper ham sauce1 sauce2// 问题:参数顺序易错,可选参数不清晰
// ❌ 重载构造函数:组合爆炸public class Pizza { public Pizza(String size) { } public Pizza(String size, String crust) { } public Pizza(String size, String crust, boolean cheese) { } public Pizza(String size, String crust, boolean cheese, boolean pepper) { } // 参数组合爆炸,维护困难}建造者模式解决方案:
// ✅ 建造者模式:流式 APIpublic class Pizza { private final String size; // 必填 private final String crust; // 必填 private final boolean cheese; // 可选 private final boolean pepper; // 可选 private final List<String> toppings; // 可选
// 私有构造器 private Pizza(Builder builder) { this.size = builder.size; this.crust = builder.crust; this.cheese = builder.cheese; this.pepper = builder.pepper; this.toppings = builder.toppings; }
// 建造者 public static class Builder { private final String size; // 必填 private final String crust; // 必填 private boolean cheese = false; // 默认值 private boolean pepper = false; private List<String> toppings = new ArrayList<>();
public Builder(String size, String crust) { this.size = size; this.crust = crust; }
public Builder cheese(boolean cheese) { this.cheese = cheese; return this; // 返回 this,支持链式调用 }
public Builder pepper(boolean pepper) { this.pepper = pepper; return this; }
public Builder topping(String topping) { this.toppings.add(topping); return this; }
public Pizza build() { // 校验逻辑 if (size == null || crust == null) { throw new IllegalArgumentException("必填参数不能为空"); } return new Pizza(this); } }}
// 使用:清晰易读Pizza pizza = new Pizza.Builder("large", "thin") .cheese(true) .pepper(false) .topping("tomato") .topping("mozzarella") .build();建造者模式 vs 工厂模式:
| 对比维度 | 工厂模式 | 建造者模式 |
|---|---|---|
| 关注点 | 创建对象的类型 | 创建对象的过程 |
| 参数 | 较少,简单 | 较多,复杂 |
| 构建过程 | 一步完成 | 分步骤进行 |
| 返回时机 | 立即返回对象 | 最后调用 build() |
| 典型应用 | Spring BeanFactory | StringBuilder |
Spring/JDK 中的建造者:
// 1. Lombok @Builder(最常用)@Builderpublic class User { private String name; private Integer age; private String email;}User user = User.builder().name("张三").age(25).build();
// 2. StringBuilderStringBuilder sb = new StringBuilder() .append("Hello") .append(" ") .append("World");String result = sb.toString();
// 3. UriComponentsBuilder(Spring)URI uri = UriComponentsBuilder .fromHttpUrl("https://api.example.com") .path("/users") .queryParam("page", 1) .queryParam("size", 10) .build() .toUri();
// 4. HttpRequest.Builder(Java 11)HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.example.com")) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(json)) .build();链式追问四:原型模式
Section titled “链式追问四:原型模式”Q8:原型模式是什么?深拷贝和浅拷贝的区别?中频
Section titled “Q8:原型模式是什么?深拷贝和浅拷贝的区别?”定义:通过复制已有对象来创建新对象,而不是通过 new。
Java 中的实现:
// 实现 Cloneable 接口public class User implements Cloneable { private String name; private Address address; // 引用类型
@Override protected User clone() throws CloneNotSupportedException { return (User) super.clone(); // 浅拷贝 }}
// 浅拷贝:只复制基本类型,引用类型仍指向原对象User user1 = new User("张三", new Address("北京"));User user2 = user1.clone();user2.setName("李四"); // 不影响 user1user2.getAddress().setCity("上海"); // 影响 user1!(address 是同一个对象)深拷贝 vs 浅拷贝:
┌────────────────────────────────────────────────┐│ 浅拷贝 │├────────────────────────────────────────────────┤│ ││ 原对象 拷贝对象 ││ ┌─────┐ ┌─────┐ ││ │name │ │name │ ││ │ ↓ │ │ ↓ │ ││ │张三 │ │李四 │ ← 独立副本 ││ └─────┘ └─────┘ ││ ││ ┌─────┐ ┌─────┐ ││ │address│──────────────→address│ ││ │ ↓ │ │ ↓ │ ││ │北京 │ │上海 │ ← 同一个对象!││ └─────┘ └─────┘ ││ │└────────────────────────────────────────────────┘
┌────────────────────────────────────────────────┐│ 深拷贝 │├────────────────────────────────────────────────┤│ ││ 原对象 拷贝对象 ││ ┌─────┐ ┌─────┐ ││ │name │ │name │ ││ │ ↓ │ │ ↓ │ ││ │张三 │ │李四 │ ← 独立副本 ││ └─────┘ └─────┘ ││ ││ ┌─────┐ ┌─────┐ ││ │address│ │address│ ││ │ ↓ │ │ ↓ │ ││ │北京 │ │北京 │ ← 独立副本! ││ └─────┘ └─────┘ ││ │└────────────────────────────────────────────────┘深拷贝实现方式:
// 方式 1:手动深拷贝@Overrideprotected User clone() throws CloneNotSupportedException { User cloned = (User) super.clone(); cloned.address = address.clone(); // 递归拷贝引用对象 return cloned;}
// 方式 2:序列化(推荐)public User deepCopy() { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this);
ObjectInputStream ois = new ObjectInputStream( new ByteArrayInputStream(bos.toByteArray())); return (User) ois.readObject(); } catch (Exception e) { throw new RuntimeException(e); }}
// 方式 3:JSON 序列化(最简单)User copy = JSON.parseObject(JSON.toJSONString(user), User.class);性能对比:
| 方式 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| 手动 clone | 最高 | 高(需要递归) | 简单对象 |
| Java 序列化 | 低 | 低 | 复杂对象图 |
| JSON 序列化 | 中 | 极低 | 不要求性能的场景 |
创建型模式总结
Section titled “创建型模式总结”四种模式对比:
| 模式 | 核心问题 | 关键特征 | Spring/JDK 应用 |
|---|---|---|---|
| 单例 | 全局唯一实例 | 私有构造器 + 静态方法 | Spring Bean 默认 scope |
| 工厂 | 对象创建逻辑复杂 | 封装创建逻辑 | BeanFactory、FactoryBean |
| 建造者 | 多参数复杂对象 | 流式 API + 分步构建 | StringBuilder、@Builder |
| 原型 | 避免重复创建相似对象 | 克隆而非 new | Object.clone() |
选择决策树:
┌────────────────────────────────────────────────┐│ 创建型模式选择 │├────────────────────────────────────────────────┤│ ││ 需要创建对象吗? ││ └── 是 ││ │ ││ ├── 需要全局唯一实例? ││ │ └── 是 → 单例模式 ││ │ ││ ├── 需要创建逻辑复杂? ││ │ ├── 一族产品 → 抽象工厂 ││ │ └── 一种产品 → 工厂方法 ││ │ ││ ├── 参数很多很复杂? ││ │ └── 是 → 建造者模式 ││ │ ││ └── 已有相似对象? ││ └── 是 → 原型模式 ││ │└────────────────────────────────────────────────┘高频面试题总结
Section titled “高频面试题总结”常见追问:
-
Q:单例模式的线程安全问题? A:饿汉式、静态内部类、枚举天然线程安全;DCL 需要 volatile;懒汉式需要同步方法。
-
Q:工厂模式增加了类的数量,值得吗? A:值得。类数量的增加换来的是开闭原则的遵守,新增产品不需要修改已有代码,降低维护成本。
-
Q:建造者模式和构造器注入如何选择? A:参数少且简单用构造器注入;参数多或有复杂构建逻辑用建造者。
-
Q:原型模式的实际应用场景? A:对象创建成本高(数据库查询、网络请求)、需要保留历史版本、避免重复初始化。