JUC + 三大设计模式(单例、工厂、代理)

标签:#Java #JUC #设计模式 #单例模式 #工厂模式 #代理模式 #面试

前言

后端面试,JUC 并发 + 设计模式 属于两大高频组合考点。JUC 解决多线程安全、并发调度问题;设计模式是工程化的代码套路,其中单例、工厂、代理 是问得最多的三个。 尤其单例模式,经常和 volatile、synchronized、多线程安全绑定在一起考察;代理模式又是 AOP、RPC、MyBatis、Spring 核心底层思想。本文把这几块串在一起,原理、代码、坑点、适用场景一次性整理。

上一篇我们梳理了 JUC 核心:锁、CAS、volatile、synchronized、线程池,本篇在此基础上,重点结合多线程视角讲解单例,再讲解工厂、代理模式。


一、快速回顾:JUC 核心要点

JUC = java.util.concurrent,JDK1.5 引入的并发工具包,底层基石 AQS。

  1. AQS :volatile int state + CLH 阻塞队列,独占 / 共享两种模式;ReentrantLock、CountDownLatch 底层都是 AQS
  2. 锁
    • 可重入锁:同一个线程可重复获取锁,synchronized、ReentrantLock 都支持
    • 公平锁:按队列顺序获取;非公平锁:上来直接抢锁,吞吐量更高,ReentrantLock 可切换,synchronized 只能非公平
  3. CAS :比较并交换,乐观无锁,底层 CPU 指令;问题:ABA、自旋耗 CPU、只能单个变量原子;配套AtomicStampedReference解决 ABA
  4. volatile :保证可见性、禁止指令重排,不保证原子性;最经典使用场景就是 DCL 单例
  5. synchronized:JVM 内置悲观锁,锁升级路线:无锁 → 偏向锁 → 轻量级锁 → 重量级锁,自动释放,不可中断
  6. 线程池 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 对象不是原子操作,分为三步:

  1. 分配堆内存
  2. 初始化对象
  3. 将 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 保证
  • 杜绝反射、序列化破坏单例的漏洞 缺点:饿汉式,类加载直接创建实例

单例破坏方式(面试附加)

  1. 反射:暴力调用私有构造器
  2. 序列化 & 反序列化

枚举天然抵御上面两种破坏;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 代理模式 代理:侧重控制访问 ;装饰器:侧重增强对象功能,结构很像,目的不同。


五、面试串讲总结(直接口述)

  1. JUC:并发工具包,AQS 为底层;锁、CAS、volatile、synchronized、线程池,解决原子性、可见性、有序性。volatile 最经典场景就是 DCL 单例,防止指令重排。
  2. 单例模式:保证全局唯一实例;饿汉、DCL(volatile)、静态内部类、枚举;枚举最安全。
  3. 工厂模式:隔离对象创建,分简单工厂、工厂方法、抽象工厂;工厂方法遵循开闭原则。
  4. 代理模式:静态代理、JDK 动态代理(需要接口)、CGLIB(子类,无需接口);SpringAOP 底层就是动态代理。

六、写在最后

设计模式不要死记代码,重点理解设计意图:

  • 单例:控制实例数量
  • 工厂:解耦对象创建
  • 代理:增强、控制访问

而 JUC 和单例的结合,是面试最喜欢的挖坑点:很多人写 DCL 忘记 volatile,这就是并发知识和设计模式的融合考点。 后续可以继续拓展:装饰器、适配器、策略模式,以及 AOP 和代理的深度源码。

相关推荐
bksczm1 小时前
Redis数据结构之hash
数据结构·redis·哈希算法·1024程序员节
2601_967987371 小时前
SvelteKit Superforms 中 SPA 模式下页面重定向问题的分析与解决
spa·1024程序员节·sveltekit·superforms·重定向问题·jsonschema
ERP管理软件1 小时前
【致远】致远协同提示当前安装版本已过期。
1024程序员节·当前版本已过期
传感器与混合集成电路1 小时前
多计算节点怎么融合?全国产通用融合计算机三子卡架构与任务配置方法
嵌入式硬件·1024程序员节
HAPPY酷1 小时前
C++ 死锁调试全流程实操手册:从复现到 GDB 定位
1024程序员节
xiangyun611 小时前
C++ priority_queue 底层:手撕二叉堆与 Top K 问题
c++·算法·面试·stl·1024程序员节
qetfw1 小时前
Linux 文件清理:find、mtime 与安全传参验证
linux·shell·1024程序员节
liubinkaixin1 小时前
一炉硬质合金的温度曲线,卡在几十根补偿导线上
物联网·1024程序员节
蓝祁子yoky1 小时前
Qt学习日记 Day8:事件系统
1024程序员节