设计模式与并发编程:线程池中的享元、Future中的代理、Lock中的状态

并发编程这块,说实话,很多设计模式都在背后默默发力。你平时用的线程池、Future、Lock,其实都是设计模式的活例子。今天我就把这几个掰开揉碎了聊聊。

一、线程池中的享元模式

享元模式的核心思想是什么?说白了就是「复用对象,减少创建」。线程池就是最典型的享元应用。

我刚开始写Java多线程时,习惯new Thread().start()。后来发现,频繁创建线程的开销太大了。线程的创建和销毁都要走系统调用,成本很高。线程池就是把这些线程对象缓存起来,重复使用。

核心要点:线程池中的工作线程就是享元对象。它们不绑定具体任务,谁来了谁用。

来看个简单的线程池实现思路:

// Java 伪代码 - 享元线程池
public class ThreadPool {
    private List<WorkerThread> workers; // 享元池
    
    public void execute(Runnable task) {
        WorkerThread worker = getFreeWorker();
        worker.assignTask(task);
    }
}

class WorkerThread extends Thread {
    private Runnable task;
    
    public void assignTask(Runnable task) {
        this.task = task;
        synchronized(this) { notify(); }
    }
    
    public void run() {
        while(true) {
            if(task != null) {
                task.run();
                task = null;
            } else {
                // 等待新任务
                synchronized(this) { wait(); }
            }
        }
    }
}

我在项目中遇到过一个问题:线程池大小设置不合理,导致大量线程竞争锁。后来我调整了核心线程数和最大线程数,配合工作队列的容量,才稳定下来。

我的习惯:CPU密集型任务,线程数设为CPU核心数+1。IO密集型任务,可以设大一些,比如2倍CPU核心数。但这只是起点,实际要压测调整。

二、Future中的代理模式

Future是什么?你提交一个异步任务,它立刻返回一个Future对象。这个Future就是真实结果的代理。

你想想看,你调用future.get()时,如果结果还没算完,线程就阻塞等待。如果算完了,直接返回。这背后的机制,就是代理模式在起作用。

// Java Future 的代理本质
public interface Future<V> {
    V get() throws InterruptedException, ExecutionException;
    boolean isDone();
}

// 代理类 - 持有真实结果或计算任务
class FutureTask<V> implements Future<V> {
    private volatile V result;    // 真实结果
    private Callable<V> callable; // 计算任务
    
    public V get() {
        if(result != null) return result;
        // 否则等待计算完成
        return waitAndGet();
    }
}

我曾经在项目里用Future做并行查询。一个请求要查三个服务,串行要300ms。改成Future并行,三个同时查,100ms就搞定了。但要注意,future.get()会阻塞,别在UI线程里调。

避坑指南:我曾经在循环里调future.get(),结果变成了串行。正确做法是先提交所有任务,收集Future列表,再统一get()。

三、Lock中的状态模式

Lock接口的实现类,比如ReentrantLock,内部维护了锁的状态。这个状态变化,就是状态模式的体现。

一个锁可能处于:未锁定、已锁定(当前线程持有)、等待(其他线程在排队)。不同状态下,lock()和unlock()的行为完全不同。

// 状态模式在Lock中的体现(简化版)
class ReentrantLock {
    private volatile int state; // 0:未锁定, 1:锁定, >1:重入
    
    public void lock() {
        if(state == 0) {
            // 未锁定 - 直接获取
            state = 1;
        } else if(Thread.currentThread() == owner) {
            // 已锁定且是当前线程 - 重入
            state++;
        } else {
            // 已锁定且不是当前线程 - 排队等待
            parkAndWait();
        }
    }
    
    public void unlock() {
        if(--state == 0) {
            owner = null;
            // 唤醒等待线程
            unparkNext();
        }
    }
}

状态模式的好处是什么?每个状态下的行为是独立的,新增状态不影响现有逻辑。比如读写锁,就比普通锁多了「读锁定」和「写锁定」两个状态。

关键理解:状态模式把「状态」和「行为」绑定在一起。Lock的state字段决定了lock/unlock的行为路径。

四、三者如何协同工作

在实际项目中,这三个模式经常一起出现。比如:

  • 线程池(享元)管理一组工作线程
  • 提交任务后返回Future(代理)
  • Future内部用Lock(状态)控制线程同步

我画了一张图,帮你理清它们的关系:

设计模式与并发编程:三者协同 线程池 享元模式 复用工作线程 减少创建/销毁开销 Future 代理模式 代理异步结果 get()时阻塞或返回 Lock 状态模式 管理锁状态 控制线程同步 提交任务 内部使用 协同工作流程 1. 线程池(享元)管理一组工作线程,避免频繁创建线程 2. 提交任务后返回Future(代理),调用方通过代理获取结果 3. Future内部用Lock(状态)控制线程同步,保证线程安全 4. 三者结合:享元复用资源,代理隔离复杂度,状态管理行为

五、实际项目中的经验

我记得有一次做高并发接口,QPS要求5000+。我用了线程池+Future的组合,但发现响应时间不稳定。排查下来,是锁竞争太激烈。

后来我做了几件事:

  • 线程池用无界队列?不行,会OOM。改成有界队列+拒绝策略
  • Future的超时时间一定要设,不然服务挂了线程一直等
  • Lock用读写锁,读多写少的场景性能提升明显

我的建议:并发编程里,设计模式不是银弹。但理解它们背后的思想,能帮你写出更健壮的代码。线程池的享元思想让你学会复用,Future的代理思想让你学会解耦,Lock的状态思想让你学会管理。

嗯,今天就聊到这。这三个模式在并发场景下非常实用,你可以在自己的项目里找找看,哪些地方可以用上。


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