设计模式与并发编程:线程池中的享元、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(状态)控制线程同步
我画了一张图,帮你理清它们的关系:
五、实际项目中的经验
我记得有一次做高并发接口,QPS要求5000+。我用了线程池+Future的组合,但发现响应时间不稳定。排查下来,是锁竞争太激烈。
后来我做了几件事:
- 线程池用无界队列?不行,会OOM。改成有界队列+拒绝策略
- Future的超时时间一定要设,不然服务挂了线程一直等
- Lock用读写锁,读多写少的场景性能提升明显
我的建议:并发编程里,设计模式不是银弹。但理解它们背后的思想,能帮你写出更健壮的代码。线程池的享元思想让你学会复用,Future的代理思想让你学会解耦,Lock的状态思想让你学会管理。
嗯,今天就聊到这。这三个模式在并发场景下非常实用,你可以在自己的项目里找找看,哪些地方可以用上。