1. 什么是线程池?
线程池和数据库的连接池的原理差不多,当需要线程工作的时候,就从线程池中获取一个空闲的线程来执行工作。当工作完成后,将线程池返回到线程池中,供其他任务使用。
![](image/2022-12-09-21-22-23.png)
2. 为什么要使用线程池?
使用线程池的优点主要有以下几个:
- 线程虽然是一个很轻量级的工具,但是创建和关闭依然需要花费一定的时间。如果每一个小任务都创建一个线程,那么很有可能创建和销毁线程的时间会大于实际工作的时间,这样得不偿失。
- 线程池可以起到管理的作用,如果无限制地创建线程,会十分消耗消耗系统的资源。因此线程池可以对线程进行管理。
3. 线程池的种类
JDK为了更好地管理线程,提供了一套Executor框架。下面是他们关系的UML图:
![](image/2022-12-09-21-29-08.png)
常见的线程池有5种,他们都是继承自ThreadPoolExecutor,所以ThreadPoolExecutor是Executor框架的核心。五种常见的线程池区别:
线程池 | 作用 |
---|---|
new FixedThreadPool(int nThreads) | 该方法返回一个固定数量的线程池,线程池中的线程的数量始终不变。 |
new SingleThreadExecutor() | 该方法返回一个只有一个线程的线程池。若有多个任务被提交到线程池,那么会保存在等待队列中。 |
new CachedThreadPool() | 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定。如果有空闲线程,那么会优先复用。如果没有线程空闲,又有任务提交,则会创建新的线程 |
new SingleThreadScheduledExecutor() | 该方法返回一个ScheduledExecutorService对象,线程池大小为1.具有延时执行的功能。 |
ScheduledThreadPoolExecutor(int corePoolSize) | 该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。 |
4. 线程池的创建
因为ThreadPoolExecutor是核心,所以创建线程池的工作也在这里完成。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
上面的函数中有很多参数,我们来逐一进行解释:
- corePoolSize:核心线程池中线程的数量,当线程池中线程的数量少于corePoolSize时,当有任务请求获得线程时,即使有空闲的线程,线程池也会继续创建线程,直到线程池中线程的个数大于等于corePoolSize为止。
- maximumPoolSize:线程池中最大的线程数量,也就是线程池中线程总数不能超过maximumPoolSize。
- keepAliveTime:空闲线程的存活时间,当线程池中的线程总数超过了corePoolSize时,如果有线程的空闲时间超过了keepAliveTime,那么就会对空闲的线程进行销毁。
- unit:时间单位,也就是空闲线程存活时间的单位。
- workQueue:阻塞队列,当线程池中的线程数量达到corePoolSize时,就会尝试将当前任务加入到阻塞队列中进行等待。例如可以使用ArrayBlockingQueue、LinkedBlockedQueue等
- threadFactory:线程工厂,可以用来自定义线程的名字。
- handler:拒绝策略,这个是当线程池中线程的数量达到maximumPoolSize,也就是达到饱和了,会执行拒绝策略。
5. 线程池的执行
我们接下来分析下线程池执行任务的流程:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 如果线程池中线程的数量小于corePoolSize
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);
// 如果线程池还活着,且线程数量为0
else if (workerCountOf(recheck) == 0)
// 创建一个没有具体命令的线程去执行等待队列中的任务
addWorker(null, false);
}
// 加入阻塞队列失败,就尝试新建线程,如果超过maximumPoolSiz就执行拒绝策略
else if (!addWorker(command, false))
reject(command);
}
通过上面的源码分析,我们可以看到线程池执行任务的流程包括3种可能:
- 当线程池中线程的数量小于corePoolSize时,直接尝试创建线程去执行当前任务。
- 当线程池中线程的数量大于等于corePoolSize时,尝试将任务加入到阻塞队列中。
- 如果进入阻塞队列失败,那么就会尝试创建新的线程执行当前任务。如果线程池中线程的数量等于maximumPoolSize,就会导致创建线程失败,只能执行拒绝策略。
流程如下图所示:
![](image/2022-12-09-21-58-18.png)
6. 线程池的关闭
线程池的关闭主要涉及两个方法:shutdown、shutdownNow。
方法 | 作用 |
---|---|
shutdown | 执行shutDown方法后,线程池不再接受新任务,等待任务执行完后关闭线程池 |
shutdownNow | 执行shutDownNow方法后,线程池不会等待任务执行完,直接关闭线程池 |
我们看下shutdown方法的源码:
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN); // 设置状态为shutDown
interruptIdleWorkers(); // 销毁空闲的线程
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
再看下shutdownNow方法的源码:
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP); // 设置状态为STOP,直接停止
interruptWorkers(); // 销毁所有线程
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
通过两段代码的对比,我们可以看到shutdownNow是不会等待线程执行完任务的,而shutdown是会等待任务执行完成。
7. 线程池使用示例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Thread thread = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "执行完任务:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println(thread.getName() + "提交任务到线程池:" + System.currentTimeMillis());
executorService.submit(thread);
}
}
}
执行结果
Thread-0提交任务到线程池:1670595179593
Thread-1提交任务到线程池:1670595179594
Thread-2提交任务到线程池:1670595179594
Thread-3提交任务到线程池:1670595179594
Thread-4提交任务到线程池:1670595179594
Thread-5提交任务到线程池:1670595179594
Thread-6提交任务到线程池:1670595179594
Thread-7提交任务到线程池:1670595179594
Thread-8提交任务到线程池:1670595179594
Thread-9提交任务到线程池:1670595179594
pool-1-thread-2执行完任务:1670595180604
pool-1-thread-1执行完任务:1670595180604
pool-1-thread-3执行完任务:1670595180604
pool-1-thread-5执行完任务:1670595180604
pool-1-thread-4执行完任务:1670595180604
pool-1-thread-1执行完任务:1670595181608
pool-1-thread-4执行完任务:1670595181608
pool-1-thread-2执行完任务:1670595181608
pool-1-thread-3执行完任务:1670595181608
pool-1-thread-5执行完任务:1670595181608
在上面的代码中,我们创建了一个固定线程数为5的线程池,同时模拟了10个任务。从执行结果可以看到,每次只有5个线程在执行任务。
8. 线程池参数配置建议
- 对于CPU密集型任务:配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。
- 对于IO密集型任务:因为IO密集型任务大部分时间都在等待IO,对CPU消耗不大,因此建议配置尽可能多的线程数量,如配置2xNcpu。