目录
Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来 3 个好处。
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的可管理性:使用线程池可以对线程进行统一分配、调优和监控
corePoolSize 是核心线程数。当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务,也会创建线程。等到需要执行的任务数大于corePoolSize 时不再创建。
线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列,这个参数就没什么效果。
超过核心线程数的线程在不执行任务时的存活时间。
keepAliveTime 的时间单位。
用于保存等待执行的任务的阻塞队列。常见阻塞队列如下:
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按 FIFO 排序元素。
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
当任务队列和线程池都满的时候采取的任务拒绝策略。
常见的拒绝策略:
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:指定调用者所在线程来运行任务
- DiscardOldestPolicy:丢弃队列中最近的一个任务,并执行当前任务
- DiscardPolicy:不处理,直接丢弃
当接收一个任务时,如果当前运行的线程数小于 corePoolSize,则创建新线程来执行任务(这一步骤需要获取全局锁)。如果运行的线程数等于或大于 corePoolSize,则将任务加入BlockingQueue。如果BlockingQueue 容量已满,则创建新的线程来处理任务(需要获取全局锁),若当前线程数大于 maxPoolSize,此时任务将会采用对应的拒绝策略。
图片来源:百度
举例:当前线程池参数如下,100 个任务同时执行
前 10 个任务创建核心线程数执行,11 - 60 个任务加入任务队列,61 - 80 个任务创建新线程执行,剩下 20 个任务直接抛弃。
- RUNNING:运行状态,线程池创建好之后就会进入此状态,如果不手动调用关闭方法,那么线程池在整个程序运行期间都是此状态。
- SHUTDOWN:关闭状态,不再接受新任务提交,但是会将已保存在任务队列中的任务处理完。
- STOP:停止状态,不再接受新任务提交,并且会中断当前正在执行的任务、放弃任务队列中已有的任务。
- TIDYING:整理状态,所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为 0 时的状态。到此状态之后,会调用线程池的 terminated() 方法。
- TERMINATED:销毁状态,当执行完线程池的 terminated() 方法之后就会变为此状态。
图片来源:百度
可以使用两个方法向线程池中提交任务,分别为 execute() 和 submit()方法。
execute() 方法用于提交不需要返回值的任务
- private static void execute() {
- poolExecutor.execute(new Runnable() {
- @Override
- public void run() {
- System.out.println("execute...");
- }
- });
- }
submit() 方法用于提交需要返回值的任务
- private static Object submit() throws ExecutionException, InterruptedException {
- Future
- @Override
- public Object call() throws Exception {
- System.out.println("submit...");
- return "hello submit";
- }
- });
-
- return submit.get();
- }
可以通过线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow 首先将线程池的状态设置为 STOP 然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析:
- 任务的性质:CPU 密集型任务、IO 密集型任务和混合任务。
- 任务的优先级:高、中、低
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
CPU 密集型任务应配置尽可能小的线程,如配置CPU数量 + 1 个线程的线程池。由于 IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 2 * CPU数量。可以通过 Runtime.getRuntime().availableProcessors()方法获得设备的CPU个数。
线程数量 = CPU数量 * CPU期望利用率 * ( 1 + wait time / service time)
wait time / service time 被称为阻塞系数,CPU 密集型任务的阻塞系数为 0
- wait time:等待IO完成时间
- service time:CPU处理任务时间
例如一个 8 核CPU,希望这部分工作的CPU使用率为 30%,任务等待 IO完成时间为 90ms,任务CPU处理时间为 10ms
线程数量 = 8 * 50% * (1 + 90 / 10) = 24