1. 为什么要使用线程池
- 线程的创建和销毁都需要消耗资源(cpu,内存)
- 使用线程池可以预先创建好线程,开始使用时速度更快
2. 线程池的设计思想
2.1. 如何做到线程在执行完任务后不回收
线程池就是要缓存线程,当一个任务执行完成之后,执行任务的线程可以不用回收,当有新的任务来到时复用当前线程来执行,平时线程执行的代码如下
java
public class Thread {
public void run() {
}
}
线程的创建和销毁实际是底层的操作系统来实现的,当启动一个线程执行任务时,run()
方法就会执行,当 run()
执行完成之后当前的线程会自动销毁 那么如何做到线程不被销毁呢?其实只要让 run()
方法一直在执行就好了,比如加个 while
循环
java
public class Thread {
public void run() {
while(true) {
}
}
}
2.2. 线程空闲时也消耗资源怎么处理
- 为了做到将线程不回收,一直执行,所以使用将任务代码放在
while(true)
里面,但是这样做会出现即使没有任务时,这个线程也会一直消耗 cpu 资源,所以需要进一步优化 - 优化方式也很简单,就是没有任务时就阻塞当前线程就好了,当有任务来到时再唤起当前线程进行执行
- 所以线程池可以认为是一个
生产者--消费者
模型,提交任务的线程就是生产者,线程池自身就是消费者,线程池从阻塞队列中不断获取任务,有任务就执行,没有任务就阻塞自己
3. 线程池创建方式
3.1. Executors 类
java
public class Executors {
// 创建固定数量线程的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 创建单个线程的线程池
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
......
}
3.2. ThreadPoolExecutor
通过上面 Executors
类创建的线程池可以看到最终还是 new ThreadPoolExecutor
, 但是直接使用 Executors
创建的线程池存在的问题是,要么线程数量没有限制,要么是队列的大小没有限制,所以一般情况下都不推荐使用 Executors
来直接创建线程池,而是手动通过 ThreadPoolExecutor
来创建, 比如下面这种方式
java
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, // 线程池核心线程数
10, // 线程池最大线程数
1000, // 线程池中超过corePoolSize数目的空闲线程最大存活时间
TimeUnit.MILLISECONDS, // 时间单位,毫秒
new LinkedBlockingQueue<>(50), // 工作线程等待队列
Executors.defaultThreadFactory(), // 自定义线程工厂
new ThreadPoolExecutor.AbortPolicy()); // 线程池满时的拒绝策略
线程池的生命周期
- RUNNING:能接受新任务,并处理阻塞队列中的任务
- SHUTDOWN:不接受新任务,但是可以处理阻塞队列中的任务
- STOP:所有任务都不处理(正在运行的线程准备中断,阻塞队列中的任务不再处理,新的任务不会再接收)
- TIDYING:所有任务都终止,并且工作线程也为0,处于关闭之前的状态
- TERMINATED:已关闭。
线程池的处理流程
线程池相关核心变量
java
public class ThreadPoolExecutor extends AbstractExecutorService {
// ctl 变量保存了两个信息,线程池的状态(高3位)和线程数量(低29位)
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 统计线程数量的位数,这里表示使用32-3 = 29 位来表示线程的数量
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// 线程池的几种状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
}
下面重点看一下 ctl 变量是如何通过一个变量存储两种信息的
线程数量
java
private static final int COUNT_BITS = Integer.SIZE - 3;
- 其中
Integer.SIZE
= 32, 所以COUNT_BITS
就是29,也就是使用 29 位来表示线程的数量
java
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
1 << COUNT_BITS
1的32位2进制是
0000 0000 0000 0000 0000 0000 0000 0001
左移29位
0010 0000 0000 0000 0000 0000 0000 0000
再进行减一
000 11111 1111 1111 1111 1111 1111 1111
所以线程池最大数目就是
000 11111 11111111 11111111 11111111
- 那怎么通过这 29 位来获取线程的数量呢,通过
workderCountOf(int c)
方法可以看到实际是通过位运算中的与
操作来去除最高位来进行获取的
java
private static int workerCountOf(int c) {
return c & COUNT_MASK;
}
线程池的状态
获取线程池状态的方法是 runStateOf()
java
private static int runStateOf(int c) {
return c & ~COUNT_MASK;
}
可以看到分成了两步
- 对
COUNT_MASK
取反,前面已经知道COUNT_MASK
前面三位是0,最后29位是1, 取反则是前面三位是1,最后29位是0 与
操作和计算线程数量时是一样的,是为了去除后面29位数,只需要计算前面三位的数即可
线程池核心问题
线程池核心线程什么时候创建
- 默认情况下不会预先创建线程,所以核心线程也是在有任务来的时候才会创建
- 在
ThreadPoolExecutor
类中提供了两个方法
java
public class ThreadPoolExecutor extends AbstractExecutorService {
// 预先启动一个核心线程
public boolean prestartCoreThread() {
return workerCountOf(ctl.get()) < corePoolSize &&
addWorker(null, true);
}
// 预先启动所有核心线程
public int prestartAllCoreThreads() {
int n = 0;
while (addWorker(null, true))
++n;
return n;
}
}
通过上面两个方法可知,在创建线程池之后(提交任务之前,也就是调用 execute 之前)是可以调用这两个方法来预先创建核心线程的
线程池的核心线程是否会被回收
java
public class ThreadPoolExecutor extends AbstractExecutorService {
public void allowCoreThreadTimeOut(boolean value) {
if (value && keepAliveTime <= 0)
throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
if (value != allowCoreThreadTimeOut) {
allowCoreThreadTimeOut = value;
if (value)
interruptIdleWorkers();
}
}
}
在 ThreadPoolExecutor
类中有提供 allowCoreThreadTimeOut(boolean)
方法来设置当达到超时时间时,核心线程也可以被回收掉
核心线程数量和最大线程数量动态修改
java
// 动态修改核心线程数量
public void setCorePoolSize(int corePoolSize) {
......
}
// 动态修改最大线程数量
public void setMaximumPoolSize(int maximumPoolSize) {
......
}
上面有写到可以动态修改线程池的核心线程和最大线程数量,那么这么设计的原因是什么呢?说到这个问题就牵扯到另外一个问题,如何合理设置线程池的线程数量大小?对于线程池数量大小的设置问题有多种建议,比如
- CPU密集型,线程池大小设置为N+1, IO密集型,线程池大小设置为2N+1(N指的是cpu核心数)
- 线程数=CPU核数 *(1+线程等待时间 / 线程时间运行时间)
对于方式2是结合了实际的业务,但是线程等待时间,线程运行时间都是需要 监控
线程环境才可以得知的,既然涉及到 监控
就说明这个值是需要动态去修改的,所以才会需要提供这样两个修改线程数量的方法
相同的任务如何舍弃
- 在某些场景下会有任务不断的往线程池中添加,但是对于添加的相同的任务如果之前还没有执行完成,后续添加的任务可以直接舍弃,这里涉及到的知识点是
队列
和对象相等的判断
- 如果每次添加新任务之前旧的任务已经执行完成,就不存在相同的任务舍弃这个问题,所以这里的场景是提交的任务太多,线程没有执行完成的任务保留在队列中
java
public class MyTask implements Runnable{
private String account;
@Override
public void run() {
doSomething();
}
// 重写 equals 方法,这个就是判断任务相同的关键
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MyTask that = (MyTask) o;
// 这里是用账号来判断任务是否相同
return account.equals(that.account);
}
}
- 提交任务之前先获取队列中的任务,看是否有正在排队的相同账号的任务
java
// 获取队列中的任务
BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
// 构建当前任务
MyTask task = new MyTask("currentTaskAccount");
if (queue.size() > 0 && queue.contains(authSymbolTask)) {
logger.info("队列中含有:{}的任务,当前任务被丢弃", task.getAccount());
} else {
// 队列中不包含当前账号的任务就提交当前任务
threadPoolExecutor.execute(task);
}
源码分析
线程池提交任务流程
下面代码是 ThreadPoolExecutor
类
java
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 线程数量小于核心线程数
if (workerCountOf(c) < corePoolSize) {
// 1. 创建线程的核心方法
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);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
可以看到添加线程进行执行主要是在 addWorker()
方法中,所以继续看 addWorkder()
java
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (int c = ctl.get();;) {
// 线程池正在关闭或已经关闭时就不运行添加新任务
if (runStateAtLeast(c, SHUTDOWN)
&& (runStateAtLeast(c, STOP)
|| firstTask != null
|| workQueue.isEmpty()))
return false;
for (;;) {
if (workerCountOf(c)
>= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateAtLeast(c, SHUTDOWN))
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// worker类包含Thread 和 Runnable Task
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int c = ctl.get();
// 线程池还在运行状态
if (isRunning(c) ||
(runStateLessThan(c, STOP) && firstTask == null)) {
if (t.getState() != Thread.State.NEW)
throw new IllegalThreadStateException();
workers.add(w);
workerAdded = true;
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 添加成功就启动线程开始执行
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
Worker 类代码如下
java
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker. */
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 如何保证线程不会被回收呢?就是通过一个 while 循环,不让线程退出 run 方法
// 如何保证线程在空闲时不占用cpu呢,实现逻辑就是在 getTask() 方法里面,如果没有获取到任务就阻塞当前线程
// 如何实现线程的回收呢?这个关键点就是 while 条件的判断,当条件不满足时就会退出 while 循环,当前线程执行完了,线程就会被jvm回收掉
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
try {
task.run();
afterExecute(task, null);
} catch (Throwable ex) {
afterExecute(task, ex);
throw ex;
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 线程池中线程被回收的逻辑
processWorkerExit(w, completedAbruptly);
}
}
线程池中的线程是如何被回收的
在上面时序图中可以看到,在 Worker.runWorker()
方法中会不断的循环,保证线程可以复用,同时在 getTask()
中做是否要退出循环的条件的判断
- 阻塞队列中没有任务了
- 已经超过了设置的空闲时间
这样就可以退出让 Thread 的run 方法流程走完,退出 run 方法之后相当于这个线程就被回收了