1. 写在前面
上一篇文章,我们介绍了线程池,这篇文章我们继续介绍线程池。只不过是更加深入,我先抛出几个问题,看看大家工作中有没有遇到过:
- 线程池的拒绝策略应该使用哪种,实际工作中是怎么用的?
- 线程池的状态有哪些?
- 阿里java开发手册中关于线程池的建议 为什么那么建议?
2. 拒绝策略的选择
线程池的拒绝策略选择应根据具体的应用场景和需求来确定。在实际工作中,不同的应用场景对拒绝策略的要求可能有所不同。
2.1 AbortPolicy(默认策略)
直接抛出 RejectedExecutionException 异常,通知调用者任务被拒绝。
适用场景:
- 当任务被拒绝时,需要立即通知调用者,并且调用者能够处理这个异常。
- 适用于对任务丢失敏感的场景,需要保证任务不被默默丢弃。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1),
new ThreadPoolExecutor.AbortPolicy()
);
2.2 CallerRunsPolicy
由调用线程来运行被拒绝的任务。
适用场景:
- 当线程池已满时,希望调用者线程自己执行任务,以减缓任务提交速度。
- 适用于需要一定的自适应机制来处理任务高峰的场景。
2.3 DiscardPolicy
直接丢弃被拒绝的任务,不抛出异常。
适用场景:
- 当可以接受任务丢失的场景,如日志记录、统计信息等非关键任务。
- 适用于对任务丢失不敏感的应用,且任务量可能会瞬间激增。
2.4 DiscardOldestPolicy
丢弃队列中最老的任务,然后重新提交被拒绝的任务。
适用场景:
- 当希望优先处理新任务,而可以接受丢弃较老任务的场景。
- 适用于任务队列中存在优先级差异的情况,确保最新任务能够被处理。
2.5 实际工作中的使用
在实际工作中,选择哪种拒绝策略通常取决于以下几个因素:
- 任务的重要性:如果每个任务都非常重要,不能丢失,那么 AbortPolicy 可能是合适的选择。
- 系统的负载能力:如果系统需要自适应高负载,CallerRunsPolicy 可以帮助平衡负载。
- 任务的性质:对于一些非关键任务,如日志记录,可以使用 DiscardPolicy 或 DiscardOldestPolicy。
- 业务需求:根据具体业务逻辑,可能需要自定义拒绝策略来满足特定需求。
2.6 自定义拒绝策略
在某些情况下,内置的拒绝策略可能无法完全满足需求,可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略。
class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 自定义处理逻辑,如记录日志、报警等
System.err.println("Task rejected: " + r.toString());
}
}
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1),
new CustomRejectedExecutionHandler()
);
3. 线程池使用最佳实践
3.1 线程池不允许使用 Executors 创建
3.1.1 原因
Executors 提供的几种创建线程池的方法存在一些潜在的问题,比如无界队列可能导致 OOM(OutOfMemoryError),固定大小线程池和单线程池可能导致任务堆积等。
3.1.2 建议
使用 ThreadPoolExecutor 来创建线程池,并明确各个参数的含义。
3.1.3 示例
// 推荐的创建线程池方式
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(queueCapacity), // 阻塞队列
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("custom-thread-name");
return thread;
}
},
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
3.2 线程池的配置要根据实际需求进行调整
3.2.1 原因
线程池的参数配置(如核心线程数、最大线程数、队列容量等)直接影响系统的性能和稳定性。
3.2.2 建议
根据具体的业务需求和系统负载情况来配置线程池的参数。
3.2.3 示例
int corePoolSize = 10;
int maximumPoolSize = 20;
long keepAliveTime = 60L;
int queueCapacity = 100;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new ThreadPoolExecutor.AbortPolicy()
);
3.3 线程池的拒绝策略要合理选择
3.3.1 原因
当线程池和队列都满了,必须要有合理的拒绝策略来处理新提交的任务,以避免系统崩溃。
3.3.2 建议
根据业务需求选择合适的拒绝策略,必要时可以自定义拒绝策略。
3.3.3 示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(queueCapacity),
new ThreadPoolExecutor.CallerRunsPolicy() // 选择CallerRunsPolicy拒绝策略
);
4. ThreadPoolExecutor
4.1 核心组件
4.1.1 主要字段
- corePoolSize:核心线程数,即线程池中始终保持存活的线程数量。
- maximumPoolSize:最大线程数,即线程池中允许的最大线程数量。
- keepAliveTime:非核心线程的存活时间,当线程池中的线程数超过核心线程数时,多余的线程在空闲时间超过 keepAliveTime 后会被终止。
- workQueue:任务队列,用于存放等待执行的任务。
- threadFactory:线程工厂,用于创建新线程。
- handler:拒绝策略,当任务无法被执行时会调用该策略。
4.1.2 内部类
- Worker:ThreadPoolExecutor 中的工作线程,继承自 AbstractQueuedSynchronizer(AQS),并实现了 Runnable 接口。
- TaskQueue:任务队列,通常是 BlockingQueue 的实现类,如 LinkedBlockingQueue、ArrayBlockingQueue 等。
4.2 任务提交与执行流程
当任务被提交到线程池时,ThreadPoolExecutor 会按照以下步骤处理:
- execute 方法:任务提交的入口方法。
- 判断当前线程池中的线程数是否小于 corePoolSize
- 如果是,则创建一个新的 Worker 并启动。
- 如果否,则将任务加入到任务队列 workQueue 中。
- 如果任务队列已满且线程数小于 maximumPoolSize
- 创建新的 Worker 并启动
- 如果线程数已达到 maximumPoolSize 并且任务队列已满
- 执行拒绝策略 handler
以下是 execute 方法的核心代码:
- 执行拒绝策略 handler
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (!isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
} else if (!addWorker(command, false))
reject(command);
}
4.3 线程池的状态控制
ThreadPoolExecutor 使用一个 AtomicInteger 类型的字段 ctl 来同时记录线程池的状态和线程数量。ctl 的高 3 位表示线程池的状态,低 29 位表示线程数量。
4.3.1 线程池状态
线程池的状态有以下几种:
- RUNNING:接受新任务并处理队列中的任务。
- SHUTDOWN:不接受新任务但处理队列中的任务。
- STOP:不接受新任务,不处理队列中的任务,并中断正在执行的任务。
- TIDYING:所有任务都已终止,workerCount 为 0,线程池将要转换为 TERMINATED 状态。
- TERMINATED:终止状态。
4.3.2 状态转换
状态转换通过 ctl 字段的高 3 位和低 29 位的位运算来实现:
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
系列文章
7.jdk源码阅读之ConcurrentHashMap(上)