设计模式与线程池:享元模式共享线程、命令模式封装任务、策略模式选择调度

线程池这个东西,说白了就是「复用线程」的容器。我刚开始做并发编程时,总觉得每次 new Thread() 挺爽的,直到线上服务被频繁创建线程搞到 OOM……嗯,从那以后我才真正理解线程池的价值。

但今天我想聊的不是线程池 API 怎么用,而是它背后藏着的三个设计模式:享元模式命令模式策略模式。你想想看,一个线程池能同时把这三个模式揉在一起,这本身就是个绝佳的教学案例。

1. 享元模式:线程池的核心思想

享元模式(Flyweight Pattern)的核心就一句话:共享细粒度对象,避免重复创建。线程池里的工作线程,就是典型的享元对象。

我习惯把线程池里的线程比作「出租车队」。每辆车(线程)可以拉不同的乘客(任务),但车本身是共享的。你不需要为每个乘客买一辆新车,对吧?

关键点:享元模式区分「内部状态」和「外部状态」。在线程池中,线程的 ID、生命周期是内部状态;而线程当前执行的任务是外部状态,由调用方传入。

// Java 中的享元线程——ThreadPoolExecutor 内部的工作线程
// 这些 Worker 对象被池化复用,不会频繁创建销毁
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    final Thread thread;        // 内部状态:线程本身
    Runnable firstTask;         // 外部状态:当前任务(可替换)
    
    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }
    
    public void run() {
        runWorker(this);  // 循环从队列取任务执行
    }
}

我在项目中遇到过一个问题:某系统需要处理大量短连接请求,如果每个请求都 new 一个线程,系统很快就扛不住了。后来改用固定大小线程池,吞吐量提升了 5 倍。这就是享元模式最直接的收益——减少资源消耗

我的建议:线程数不是越多越好。CPU 密集型任务,线程数 = CPU 核心数 + 1 就够了;IO 密集型可以适当多些,但也要考虑上下文切换的开销。

2. 命令模式:把任务封装成对象

命令模式(Command Pattern)把「请求」封装成对象,让调用方和接收方解耦。在线程池里,每个 Runnable 或 Callable 就是一个命令对象。

你想想看,线程池的 submit() 方法接收的是什么?是一个任务对象。它不关心这个任务具体做什么,只负责把它丢到队列里,让空闲线程去执行。这就是命令模式的精髓——请求的发起者和执行者分离

// 命令模式在线程池中的体现
// 调用方:提交任务
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(new Callable<String>() {
    @Override
    public String call() throws Exception {
        // 这是命令的具体执行逻辑
        return "任务完成";
    }
});

// 接收方:线程池内部
// 命令被封装成 FutureTask 对象,放入工作队列
// 工作线程从队列取出 FutureTask,执行其 run() 方法

我记得有一次做批处理系统,需要支持任务的重试、取消、超时控制。如果直接用裸线程,这些功能都得自己手写。但用线程池 + Future 模式,命令对象天然支持这些操作——因为命令本身就是一个可管理的对象。

我曾经踩过的坑:命令对象里如果持有大对象引用,会导致内存泄漏。因为任务执行完后,线程池可能还持有这个命令对象的引用(比如在队列里等待)。所以任务对象最好设计成轻量级的,或者执行完后主动释放资源。

3. 策略模式:调度算法的选择

策略模式(Strategy Pattern)定义一系列算法,把它们封装起来,并且可以互相替换。线程池的拒绝策略(RejectedExecutionHandler)就是最典型的例子。

当线程池满了,新任务怎么办?Java 提供了四种内置策略:

策略名称 行为 适用场景
AbortPolicy 直接抛异常 关键任务,不能丢失
CallerRunsPolicy 调用者线程自己执行 降低任务提交速度
DiscardPolicy 默默丢弃 不重要的任务
DiscardOldestPolicy 丢弃队列中最旧的任务 消息类场景,新任务更重要

我习惯在项目中自定义策略。比如有一个日志处理系统,我不希望因为线程池满了就丢日志,但也不想阻塞主流程。于是我写了一个策略:把任务暂存到本地文件,等线程池有空了再加载回来。这就是策略模式的灵活性——你可以随时替换算法,而不影响线程池的其他部分。

// 自定义拒绝策略——策略模式的典型应用
public class FileBackupPolicy implements RejectedExecutionHandler {
    private static final String BACKUP_DIR = "/tmp/task_backup/";
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 将任务序列化到文件
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream(BACKUP_DIR + UUID.randomUUID() + ".task"))) {
            oos.writeObject(r);
            System.out.println("任务已备份到文件");
        } catch (Exception e) {
            // 如果文件备份也失败,才抛异常
            throw new RejectedExecutionException("备份失败", e);
        }
    }
}

// 使用自定义策略
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(10),
    new FileBackupPolicy()  // 注入策略
);

4. 三个模式如何协同工作?

这三个模式在线程池里不是孤立的,它们配合得天衣无缝:

  • 享元模式提供「资源池」,让线程可复用
  • 命令模式提供「任务封装」,让任务可排队、可管理
  • 策略模式提供「调度决策」,让线程池在边界情况下有章可循

说白了,线程池就是一个「享元工厂 + 命令队列 + 策略注入」的组合体。你想想看,如果没有享元模式,线程频繁创建销毁,性能崩了;如果没有命令模式,任务无法排队和取消;如果没有策略模式,线程池满了就只能干瞪眼。

核心总结:设计模式不是孤立存在的理论,它们在实际框架中往往是「组合拳」。学会识别这些模式,你就能看懂框架的设计意图,甚至自己写出高质量的并发组件。

线程池中的三种设计模式协同 调用方(提交任务) submit(Runnable) 命令模式:封装任务 Runnable / Callable / FutureTask 放入队列 工作队列(BlockingQueue) Worker 取出执行 享元模式:复用线程(Worker 池) 策略模式 拒绝策略选择 AbortPolicy CallerRunsPolicy DiscardPolicy 自定义策略...

实践建议:如果你要自己实现一个简单的线程池,不妨从这三个模式入手。先写一个享元模式的线程复用框架,再引入命令模式封装任务,最后加上策略模式处理边界情况。一步步来,你会发现设计模式其实就在身边。

公众号:蓝海资料掘金营,微信deep3321