性能监控
- 监控队列深度:
ThreadPoolExecutor
提供了 getQueue().size()
方法来获取当前任务队列的大小,可定时调用此方法获取队列深度指标。
- 也可以通过继承
ThreadPoolExecutor
类,重写 beforeExecute
和 afterExecute
方法,在任务进入队列和从队列取出执行时记录队列大小的变化。
- 监控任务执行耗时:
- 继承
ThreadPoolExecutor
类,重写 beforeExecute
和 afterExecute
方法。在 beforeExecute
方法中记录任务开始时间,在 afterExecute
方法中记录任务结束时间,两者相减得到任务执行耗时。
- 使用
ThreadLocal
变量来存储每个任务的开始时间,避免多线程并发访问的问题。例如:
public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
private static final ThreadLocal<Long> startTime = new ThreadLocal<>();
public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
startTime.set(System.currentTimeMillis());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
Long endTime = System.currentTimeMillis();
Long executeTime = endTime - startTime.get();
startTime.remove();
System.out.println("任务执行耗时:" + executeTime + " ms");
}
}
- 其他指标监控:
getActiveCount()
:获取当前正在执行任务的线程数。
getCompletedTaskCount()
:获取已完成任务的总数。
getCorePoolSize()
:获取核心线程数。
getMaximumPoolSize()
:获取最大线程数。
getPoolSize()
:获取当前线程池的线程数。
性能瓶颈调优
- 线程池参数调优:
- 核心线程数(
corePoolSize
):
- 如果任务执行时间短且并发量大,可适当增加核心线程数,让更多任务能立即执行,减少任务在队列中的等待时间。
- 如果任务执行时间长,核心线程数不宜设置过大,否则可能导致系统资源过度消耗。可以通过压测,逐步调整核心线程数,观察系统吞吐量和响应时间的变化,找到最优值。
- 最大线程数(
maximumPoolSize
):
- 如果任务队列经常满,并且任务执行时间较短,可以适当增加最大线程数,以便在高峰期处理更多任务。但设置过大可能会导致系统资源耗尽,因为每个线程都需要占用一定的内存等资源。
- 同样通过压测,监控线程池的队列深度、任务拒绝率等指标,调整最大线程数,平衡系统资源和任务处理能力。
- 线程存活时间(
keepAliveTime
):
- 如果任务执行频率不稳定,有明显的波峰波谷,可以适当延长线程存活时间,让线程在任务低谷期不被销毁,以便在高峰期快速处理任务,减少线程创建和销毁的开销。但存活时间过长也会占用系统资源,需要根据实际业务场景调整。
- 任务队列:
- 无界队列(如
LinkedBlockingQueue
):使用无界队列时,任务会不断进入队列而不会触发拒绝策略,可能导致内存耗尽。如果任务执行速度较慢,且任务量持续增长,可考虑更换为有界队列。
- 有界队列(如
ArrayBlockingQueue
):根据任务的预计数量和系统资源,合理设置队列容量。如果队列容量过小,可能导致任务频繁被拒绝;容量过大,可能会使任务在队列中等待时间过长,影响响应时间。
- 优先队列(如
PriorityBlockingQueue
):如果任务有优先级之分,可以使用优先队列,让高优先级任务先执行,提高系统整体性能。
- 拒绝策略:
- 默认拒绝策略(
AbortPolicy
):直接抛出异常,适用于需要严格保证任务执行的场景,如果任务被拒绝会导致业务失败。
- 丢弃策略(
DiscardPolicy
):丢弃被拒绝的任务,适用于对任务执行结果不敏感的场景。
- 丢弃最旧任务策略(
DiscardOldestPolicy
):丢弃队列中最旧的任务,尝试提交新任务,适用于任务执行时间较短且需要保证新任务尽快执行的场景。
- 自定义拒绝策略:根据业务需求实现
RejectedExecutionHandler
接口,定制化处理被拒绝任务的逻辑,例如将任务写入日志、发送消息通知等。
- 硬件资源调优:
- CPU 资源:
- 检查 CPU 使用率,如果 CPU 使用率过高,可能是任务计算量过大。可以考虑优化任务代码,减少不必要的计算,或者采用并行计算框架(如
Fork/Join
框架)进一步提高 CPU 利用率。
- 如果是多核 CPU,可以适当增加线程池的线程数,充分利用多核优势,但要注意线程竞争带来的开销。
- 内存资源:
- 如果任务需要大量内存,确保系统有足够的物理内存。如果物理内存不足,可能导致频繁的磁盘交换,严重影响性能。可以通过增加物理内存或优化任务内存使用来解决。
- 对于线程池中的任务,如果涉及对象的频繁创建和销毁,可以考虑使用对象池技术,减少内存分配和垃圾回收的开销。
- I/O 资源:
- 如果任务中有大量 I/O 操作(如文件读写、网络通信等),I/O 操作可能成为性能瓶颈。可以采用异步 I/O 技术(如 Java NIO),提高 I/O 操作的并发性能。
- 优化 I/O 操作的方式,例如批量读写数据,减少 I/O 操作的次数。
- 代码层面优化:
- 减少锁竞争:如果任务中存在锁操作,检查是否可以通过优化锁的粒度、使用读写锁(
ReadWriteLock
)或并发容器(如 ConcurrentHashMap
)等方式减少锁竞争,提高并发性能。
- 优化算法和数据结构:对任务内部使用的算法和数据结构进行优化,提高任务执行效率,从而间接提升线程池的性能。例如,使用更高效的排序算法、选择合适的数据结构存储数据等。