线程池是一种生产者/消费者模式的实现.
线程池处理任务的流程
ThreadPoolExecutor
是一种线程池的实现, 它执行execute()
的处理流程如下:
上图中与新创建线程有关的步骤都需要获取全局锁, 所以线程池中应当尽量避免进行新线程的创建.
实际上在ThreadPoolExecutor
完成预热之后(corePoolSize已满)的时候, 几乎所有的execute()
方法都是执行入队操作.
这样就避免了全局锁的获取(注意: 并不是说入队不需要获取锁, 只是这时候获取的不是全局锁而已).
线程池的使用
线程池的创建
可以通过ThreadPoolExecutor
来创建一个线程池:
new ThreadPoolExecutor(
corePoolSize, // 线程池的基本大小, 小于此数值时仅新建线程
maximumPoolSize,
keepAliveTime, // 线程池的工作线程空闲时, 保持存活的时间.
milliseconds,
runnableTaskQueue, // 任务队列, 需要使用阻塞队列
handler); // 当线程池和队列都满了的时候的丢弃策略
向线程池提交任务
两种方法:
- 使用
execute()
, 适用于提交不需要返回值的任务, 所以也无法判断任务是否被线程池执行成功;
threadPool.execute(new Runnable() {
@override
public void run() {
// 这里是具体的代码
}
});
- 使用
submit()
, 适用于提交需要返回值的任务, 会返回一个Future
类型的对象, 用于判断任务是否执行成功.
可以通过Future
对象的get()
方法来获取返回值.get()
会阻塞当前线程直到任务完成.
Future<Object> future = threadPool.execute(hasReturnValueTask);
try {
Object o = future.get();
} catch(Exception e) {
} finally {
threadPool.shutdown();
}
关闭线程池
通常调用shutdown()
方法来关闭线程池, 确保所有正在执行的任务都正常完成;
如果当前任务不一定要执行完, 也可以使用shutdownNow()
来进行.
合理的配置线程池
以下以CPU的总核心数为n来计算
核心线程数
需要根据任务的性质来具体划分
- 如果是计算密集型任务(也就是CPU密集型任务, 大量快速执行的小任务), 则配置n+1个线程, 充分利用CPU的计算能力.
- 如果是IO密集型任务, 则单任务的等待时间长, 则应该配置尽可能多的线程, 比如2*n
- 如果是混合型的任务, 则可以配置两个不同规模的线程池, 也可以使用优先级队列, 让执行时间短的任务先执行;
队列的选择
可以使用优先级队列PriorityBlockingQueue
来处理, 让优先级高的任务先执行.
另外, 建议使用有界队列, 因为在遇到IO问题时, 有界队列只会发出抛弃任务的异常,
但如果使用了无界队列, 则有可能不断创建线程, 最终导致内存溢出, 影响其他线程.
线程池的监控
监控线程池的运作方便在出现问题时, 可以根据线程池的使用状况快速定位问题, 也有利于进行调优.
在监控线程池的时候有如下属性:
-
taskCount
: 需要执行的任务量; -
completedTaskCount
: 已完成的任务量, 小于等于taskCount
; -
largestPoolSize
: 池中的历史最大线程数量, 可以据此判断线程池是否满过, 也就是是否用过队列; -
getPoolSize
: 得到线程池的当前线程数量, 只增不减的一个值. -
getActiveCount
: 获取活动的线程数;
要使用以上属性, 需要创建线程池实现类的子类, 并重写beforeExecute, afterExecute, terminated
方法.
例如: 监控任务的平均执行时间, 最大执行时间, 最小执行时间等.