标签:#Java #JUC #设计模式 #单例模式 #工厂模式 #代理模式 #面试
前言
后端面试,JUC 并发 + 设计模式 属于两大高频组合考点。JUC 解决多线程安全、并发调度问题;设计模式是工程化的代码套路,其中单例、工厂、代理 是问得最多的三个。 尤其单例模式,经常和 volatile、synchronized、多线程安全绑定在一起考察;代理模式又是 AOP、RPC、MyBatis、Spring 核心底层思想。本文把这几块串在一起,原理、代码、坑点、适用场景一次性整理。
上一篇我们梳理了 JUC 核心:锁、CAS、volatile、synchronized、线程池,本篇在此基础上,重点结合多线程视角讲解单例,再讲解工厂、代理模式。
一、快速回顾:JUC 核心要点
JUC = java.util.concurrent,JDK1.5 引入的并发工具包,底层基石 AQS。
- AQS :
volatile int state+ CLH 阻塞队列,独占 / 共享两种模式;ReentrantLock、CountDownLatch 底层都是 AQS - 锁
- 可重入锁:同一个线程可重复获取锁,synchronized、ReentrantLock 都支持
- 公平锁:按队列顺序获取;非公平锁:上来直接抢锁,吞吐量更高,ReentrantLock 可切换,synchronized 只能非公平
- CAS :比较并交换,乐观无锁,底层 CPU 指令;问题:ABA、自旋耗 CPU、只能单个变量原子;配套
AtomicStampedReference解决 ABA - volatile :保证可见性、禁止指令重排,不保证原子性;最经典使用场景就是 DCL 单例
- synchronized:JVM 内置悲观锁,锁升级路线:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,自动释放,不可中断
- 线程池 ThreadPoolExecutor:7 大参数、任务执行流程、4 种拒绝策略,生产禁止直接用 Executors
重点衔接:volatile + synchronized 就是为了写出线程安全的单例。
二、单例模式(Singleton)
核心目标:保证一个类在整个 JVM 中,只有且仅有一个实例,对外提供全局访问点 。 考点重点:多线程安全、懒汉 / 饿汉、DCL 双重检查锁、静态内部类、枚举单例。
2.1 饿汉式(静态常量,线程安全,最简单)
类加载的时候直接初始化实例,类加载天然线程安全;缺点:类加载就创建,不管用不用,容易浪费内存。
java
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
// 私有构造,防止外部new
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return INSTANCE;
}
}
2.2 懒汉式(普通版本,线程不安全,面试反面例子)
用到的时候才创建,多线程并发调用会创建多个实例,并发下直接失效。
java
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
2.3 DCL 双重检查锁(Double-Check Locking,⭐高频)
必须加 volatile!
java
public class DclSingleton {
// volatile 禁止指令重排,防止拿到半初始化对象
private static volatile DclSingleton instance;
private DclSingleton(){}
public static DclSingleton getInstance(){
// 第一次判断:减少锁竞争
if(instance == null){
synchronized (DclSingleton.class){
// 第二次判断:防止多个线程等待锁之后重复创建
if(instance == null){
instance = new DclSingleton();
}
}
}
return instance;
}
}
为什么要 volatile? new 对象不是原子操作,分为三步:
- 分配堆内存
- 初始化对象
- 将 instance 引用指向内存地址 编译器可能重排成 1 →3 →2,别的线程拿到引用时,对象还没初始化完成,出现 NPE / 异常。volatile 通过内存屏障禁止这个重排。
2.4 静态内部类单例(推荐,兼顾懒加载 + 线程安全)
利用类加载机制,外部类加载不会加载内部类,调用 getInstance 才加载,天然线程安全,不需要锁。
java
public class InnerClassSingleton {
private InnerClassSingleton(){}
private static class SingletonHolder{
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
2.5 枚举单例(Effective Java 推荐,防反射、防序列化破坏)
java
public enum EnumSingleton {
INSTANCE;
public void businessMethod(){
System.out.println("枚举单例方法");
}
}
// 使用:EnumSingleton.INSTANCE.businessMethod();
优点:
- 天然单例,JVM 保证
- 杜绝反射、序列化破坏单例的漏洞 缺点:饿汉式,类加载直接创建实例
单例破坏方式(面试附加)
- 反射:暴力调用私有构造器
- 序列化 & 反序列化
枚举天然抵御上面两种破坏;DCL、静态内部类需要额外处理。
选型小结
- 简单、不怕提前加载:饿汉
- 懒加载、高性能:静态内部类
- 需要懒加载,又想按需控制:DCL(记住 volatile)
- 追求安全、防反射:枚举
三、工厂模式
工厂模式核心思想:把对象创建逻辑抽离出来,使用者不用关心 new 的细节,面向产品抽象,而不是直接 new 具体类 。 分为三层:简单工厂(不属于 GOF23,但是常讲)、工厂方法、抽象工厂
3.1 简单工厂(静态工厂)
一个工厂类,根据参数,生产不同产品。 适合产品少、变化不大场景;缺点:新增产品要修改工厂代码,违反开闭原则。
java
// 产品接口
interface Car{
void run();
}
// 具体产品
class Bmw implements Car{
@Override
public void run() { System.out.println("宝马奔跑"); }
}
class Audi implements Car{
@Override
public void run() { System.out.println("奥迪奔跑"); }
}
// 工厂
class CarFactory{
public static Car getCar(String type){
if("bmw".equals(type)) return new Bmw();
else if("audi".equals(type)) return new Audi();
return null;
}
}
3.2 工厂方法模式
一个产品对应一个工厂,工厂也抽象。新增产品,新增产品类 + 新增工厂,不修改原有代码,满足开闭原则。 缺点:类爆炸,产品一多,工厂类成倍增加。
java
interface Car{
void run();
}
interface CarFactory{
Car createCar();
}
class Bmw implements Car{@Override public void run(){}}
class BmwFactory implements CarFactory{
@Override
public Car createCar() { return new Bmw(); }
}
3.3 抽象工厂模式
工厂生产一族相关产品,而不是单个产品。比如汽车工厂同时生产轿车、SUV。 适合产品族固定的场景;一旦产品族要新增产品,改动巨大。
工厂模式一句话区分
- 简单工厂:一个工厂造多种产品
- 工厂方法:一个工厂只造一种产品,工厂和产品一一对应
- 抽象工厂:一个工厂造一套系列产品
适用场景:对象创建逻辑复杂、创建逻辑多处复用、希望隔离创建和使用。 Spring 里面 BeanFactory 就是工厂思想。
四、代理模式(Proxy)⭐重点,和 AOP 强相关
核心:给目标对象提供一个代理对象,由代理对象控制对原对象的访问,可以在不修改原目标代码的前提下,增加额外逻辑(前置、后置、异常) 分类:静态代理、JDK 动态代理、CGLIB 动态代理
Spring AOP 底层就是动态代理:目标有接口用 JDK 代理,无接口用 CGLIB。
4.1 静态代理
代理类在编译期手写出来,代理类和目标类实现同一个接口。 缺点:接口一变,代理类同步改;接口很多时代理类爆炸。
java
// 接口
interface UserService{
void addUser();
}
// 目标类
class UserServiceImpl implements UserService{
@Override
public void addUser() {
System.out.println("新增用户");
}
}
// 静态代理类
class UserServiceProxy implements UserService{
private UserService target;
public UserServiceProxy(UserService target){
this.target = target;
}
@Override
public void addUser() {
System.out.println("前置:日志");
target.addUser();
System.out.println("后置:日志");
}
}
4.2 JDK 动态代理(接口代理)
不需要手写代理类,运行期由 JDK 自动生成代理类;强制要求目标对象必须实现接口 。 核心 API:java.lang.reflect.Proxy + InvocationHandler
java
interface UserService{
void addUser();
}
class UserServiceImpl implements UserService{
@Override
public void addUser() {
System.out.println("新增用户");
}
}
class MyInvocationHandler implements InvocationHandler {
private final Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK代理前置逻辑");
Object res = method.invoke(target, args);
System.out.println("JDK代理后置逻辑");
return res;
}
}
// 使用
public class Test {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new MyInvocationHandler(target)
);
proxy.addUser();
}
}
4.3 CGLIB 代理(子类代理,不需要接口)
底层 ASM 字节码框架,运行时动态生成目标类的子类,重写方法完成代理; 要求:目标类不能是 final,方法不能是 final。 Spring 中当类没有实现接口,自动切换 CGLIB。
代理模式使用场景
日志、权限校验、事务控制、监控埋点、RPC 远程调用、MyBatis Mapper 代理。
区分:装饰器模式 vs 代理模式 代理:侧重控制访问 ;装饰器:侧重增强对象功能,结构很像,目的不同。
五、面试串讲总结(直接口述)
- JUC:并发工具包,AQS 为底层;锁、CAS、volatile、synchronized、线程池,解决原子性、可见性、有序性。volatile 最经典场景就是 DCL 单例,防止指令重排。
- 单例模式:保证全局唯一实例;饿汉、DCL(volatile)、静态内部类、枚举;枚举最安全。
- 工厂模式:隔离对象创建,分简单工厂、工厂方法、抽象工厂;工厂方法遵循开闭原则。
- 代理模式:静态代理、JDK 动态代理(需要接口)、CGLIB(子类,无需接口);SpringAOP 底层就是动态代理。
六、写在最后
设计模式不要死记代码,重点理解设计意图:
- 单例:控制实例数量
- 工厂:解耦对象创建
- 代理:增强、控制访问
而 JUC 和单例的结合,是面试最喜欢的挖坑点:很多人写 DCL 忘记 volatile,这就是并发知识和设计模式的融合考点。 后续可以继续拓展:装饰器、适配器、策略模式,以及 AOP 和代理的深度源码。