自己动手写动态线程池框架 02——自定义可变容量的阻塞队列
考虑到JDK原生阻塞队列的容量不可变性与线程池动态调参需求存在根本性冲突,动态线程池框架需要自定义可变容量的阻塞队列。
一、原生阻塞队列的致命缺陷:静态容量
JDK提供的常用阻塞队列(如ArrayBlockingQueue
、LinkedBlockingQueue
)均在构造时固定容量:
// 容量一旦设定即不可修改
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(100);
这意味着:
- 无法运行时扩容:当流量突增时,即使线程池已动态调大
maximumPoolSize
,任务仍因队列满被拒绝 - 无法运行时缩容:低峰期需释放内存时,无法缩小队列占用空间
二、动态线程池的核心诉求:资源弹性
动态线程池的核心价值在于根据系统负载实时调整资源:
graph LR
A[监控指标] -->|队列堆积| B(扩容线程数)
A -->|队列持续空| C(缩容线程数)
B --> D{队列满?}
D -->|是| E[需扩容队列容量]
此时暴露矛盾:
- 线程数可动态调整:通过
setMaximumPoolSize()
实时生效 - 队列容量仍固定:成为系统弹性能力的瓶颈
三、解决方案:自定义可变容量队列
通过重写阻塞队列实现运行时动态调整容量:
public class ResizableBlockingQueue<T> extends LinkedBlockingQueue<T> {
// ……省略其他代码,基本与 jdk 父类一致
// 动态更新capacity的方法
public void setCapacity(int capacity) {
final int oldCapacity = this.capacity;
//给capacity成员变量赋值
this.capacity = capacity;
final int size = count.get();
if (capacity > size && size >= oldCapacity) {
//因为队列扩容了,所以可以唤醒阻塞的入队线程了
signalNotFull();
}
}
// 增加唤醒入队线程的方法
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
}
动态调优过程示例:
// 初始化动态队列(初始容量=50)
ResizableBlockingQueue queue = new ResizableBlockingQueue(50);
// 监控到队列持续满载时
if (queue.isFull()) {
// 动态扩容队列(避免触发拒绝策略)
queue.setCapacity(100);
// 同步扩容线程数(双维度弹性)
executor.setMaximumPoolSize(200);
}
四、自定义队列的核心价值
1. 突破资源死锁困境
场景 | 原生队列 | 动态队列 |
---|---|---|
突发流量 + 队列满 | 触发拒绝策略丢任务 | 即时扩容队列避免任务丢失 |
低峰期内存回收 | 队列持续占用内存 | 缩容队列释放内存 |
2. 实现精准流量控制
graph TD
F[流量探测器] -->|队列使用率>90%| G[队列扩容+线程扩容]
F -->|队列使用率<30%| H[队列缩容+线程缩容]
G --> I[避免任务拒绝]
H --> J[减少内存占用]
3. 保证弹性策略完整性
线程池弹性 = 线程数弹性 + 队列容量弹性
二者缺一即会导致:
- 仅线程扩容 → 队列满载时新线程无用武之地
- 仅队列扩容 → 消费者不足导致响应延迟飙升
五、生产环境注意事项
-
容量缩容安全机制
public synchronized void setCapacity(int newCapacity) { // 禁止缩容到小于当前元素数 if (newCapacity < this.size()) { throw new IllegalStateException("Can't reduce below current size"); } ... }
-
避免频繁震荡
增加扩容/缩容的冷却时间(如5分钟内仅允许调整1次)