什么是线程池?
顾名思义,就是一个存放线程的池子,这个池子可以限制和管理线程资源。
线程池的好处
在工作开发过程中,线程池是经常用到的并发框架,那么合理地使用线程池可以大大提高执行速度,给我们带来以下好处:
- 降低资源消耗。通过重复利用已创建的线程资源降低线程创建和销毁造成消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
线程池的使用
我们可以通过 ThreadPoolExecutor 创建线程池,创建线程池需要的一些参数:
三个重要的参数:
- corePoolSize(线程池的基本大小):当提交一个任务到达线程池时,线程池会创建一个线程来执行任务,即使其他空闲的线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池的基本大小时就不再创建。
- maximumPoolSize(线程池的最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,线程池会再创建新的线程执行任务。如果使用了无界队列这个参数是没有任何效果的,因为任务会一直添加进队列里。
- workQueue(任务队列):用来保存等待执行任务的阻塞队列。
其他参数:
- ThreadFactory(创建线程的工厂):最好给每个线程设置一个有意义的名字。
- keepAliveTime(线程存活的时间):当我们的线程池的数量大于 corePoolSize 时,此时没有新的任务提交,核心线程外的其他线程不会立即销毁,会等到时间超过了 keepAliveTime 才会被回收。
- RejectedExecutionHandler(饱和策略):当队列和线程池都满了,线程池处于饱和状态,那么必须采取一种策略去处理提交的新任务。
- TimeUnit(线程活动保持时间的单位):DAYS、HOURS、MINUTES、MILLISECONDS、MICROSECONDS、NANOSECONDS。
向线程池提交任务
我们可以使用两个方法向线程池提交任务,execute() 和 submit() 方法。
- execute 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行完成。
- submit 方法用于提交需要返回值的任务。线程池会返回一个 future 对象,通过这个对象我们可以判断任务是否执行完成。
ThreadPoolExecutor的运行状态
- RUNNING:能够接受提交的任务,并且能够处理阻塞队列中的任务。
- SHUTDOWN:不再接受新提交的任务,但是可以继续处理阻塞队列中的已保存的任务。
- STOP:不再接受新提交的任务,也不处理阻塞队列中的任务,会中断处理任务的线程。
- TIDYING:所有的任务都终止了,workerCount(有效线程数)为0。
- TERMINATED:在 terminated() 方法执行之后进入该状态。
关闭线程池
我们可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们是有区别的,shuwdownNow 方法首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而 shutdown 只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行状态的线程。只要调用了这两个关闭方法中的任意一个,isShutdown 方法就会返回 true。当所有任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 true。
线程池的实现原理
当向线程池提交一个任务之后,处理流程如图所示:
- 如果当前运行的线程小于 corePoolSize(核心线程池数),则创建新线程来执行任务(需要获取全局锁)。
- 如果运行的线程等于或大于 corePoolSize,则将任务加入到BlockingQueue(阻塞队列)中。
- 如果 BlockingQueue 满了的话,则创建新线程来执行任务(需要获取全局锁)。
- 如果创建新线程将使当前运行的线程大于 maximumPoolSize(最大线程数),那么该任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution() 方法。
合理分配线程池
线程池大小不是设置的过大或者过小都不好,如果设置的太小的话,可能同一时间有很多任务提交,这样导致大量任务堆积在阻塞队列中,甚至出现队列满了无法处理任务的情况,如果设置的太大,大量线程争夺 CPU 资源导致频繁的上下文切换,影响效率,所以线程池的大小要合理才是最好,合理分配线程池要先分析任务的特性,我们可以从这几个方向去分析:
- 任务的性质:CPU 密集型任务、IO密集型任务和混合型任务。
- 任务的优先级
- 任务的执行时间
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
CPU 密集型任务应配置尽可能小的线程,如配置 N(CPU 核心数)+ 1。因为 IO 密集型任务的线程并不是一直在执行任务的,所以我们可以尽可能配置多的线程,当线程在处理 IO 的时候,可以把 CPU 让给其他线程使用,如配置 2 * N(CPU 核心数)。这只是一个简单的公式,IO密集型和CPU密集型的任务运行起来的情况差异非常大,实际应用中还是要根据实际情况去调整这些参数。
CPU 密集型可以简单理解为利用 CPU 的计算能力去完成一些计算,如视频编码、代码数学计算。涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待 IO 操作完成。
优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。
执行时间不同的任务可以交给不同规模的线程池来处理,也可以使用优先级队列,时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,等待的时间越长,CPU 空闲的时间也越长,那么线程数可以设置大一点,更好地利用 CPU。
线程池的监控
在系统中如果大量使用到了线程池,非常有必要对线程池进行监控,这样方便我们在出现问题时,可以快速根据线程池的情况定位问题并解决。
动手实现案例
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 5; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " StartTime = " + new Date());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " EndTime = " + new Date());
}
});
}
// 线程池需要执行的任务数量
System.out.println("线程池需要执行的任务数量:" + executor.getTaskCount());
// 线程池里曾经创建过的最大线程数量
System.out.println("线程池里曾经创建过的最大线程数量:" + executor.getLargestPoolSize());
// 获取活动的线程数
System.out.println("获取活动的线程数:" + executor.getActiveCount());
// 线程池在运行过程中已完成的任务数量
System.out.println("线程池在运行过程中已完成的任务数量:" + executor.getCompletedTaskCount());
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程池在运行过程中已完成的任务数量:" + executor.getCompletedTaskCount());
// 终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
System.out.println("isTerminated");
}
}
}
参考资料
《Java 并发编程的技术》
推荐阅读
《Java 并发编程的技术》