最近在看java线程池的实现,主要是ThreadPoolExecutor,因此写个博客记录下来。
假设我们创建了一个线程池,其中核心线程数为3,最大线程数为6,任务队列的长度为5。
ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 6, 1, TimeUnit.HOURS, new ArrayBlockingQueue<>(5));
那么根据ThreadPoolExecutor
的doc描述来看,我们可以得到以下的信息
核心线程数和最大线程数
- 当我们往pool中添加任务,也就是执行
execute(Runnable)
方法时
- 如果此时pool中的线程数少于3,一个新的线程就会被创建,即使当前pool中存在空闲的线程
- 如果此时pool中的线程数大于3,小于6,那么当前仅当任务队列满的时候,才会有新的线程被创建
- 线程池的核心线程数以及最大线程数在运行过程中,也可以动态设定,通过执行
pool.setCorePoolSize(10);
pool.setMaximumPoolSize(10);
- 我们这里设定了一个固定大小的线程池,也就是
fixed-size thread pool
新线程的创建
-
在线程池构造好之后,线程并不会被马上创建好,只有当有任务提交到线程池中,新线程才会被创建然后开始运行,不过我们可以调用
prestartCoreThread
或prestartAllCoreThreads
方法,提前创建出新线程。 -
线程的创建会由线程工厂来负责,线程工厂可以是由我们指定,也可以使用默认的线程工厂,以默认的线程工厂为例,其创建的所有线程均在同一个线程组中,并且拥有相同的优先级(
NORM_PRIORITY
),同时均是非守护线程(这也导致线程池不会自己停掉)。
超时时间
- 当前池中的线程数为5,并且有3个线程的空闲时间超过了
keepAliveTime
,那么会有2个线程被停止 - 我们也可以在程序执行中动态的设定超时时间
pool.setKeepAliveTime(30, TimeUnit.MINUTES);
- 我们也可以用同样的策略停止核心线程数的线程
pool.allowCoreThreadTimeOut(true);
任务入队
任务入队的方式和当前线程池的大小有关系,下面具体情况说明
- 假如当前有2个线程在运行,通常线程池会新建一个线程,而不是让提交的任务入队
- 假如当前有3个线程在运行,通常线程池会让提交的任务入队,而不是新建一个线程
- 如果当前队列已满,并且当前线程池没有超过最大线程数,此时会创建一个新的线程;如果当前线程数达到了最大线程数,此时会触发拒绝策略。
勾子方法
重写以下的方法,在执行任务前和执行任务后,做一些定制的操作。
beforeExecute(Thread, Runnable)
afterExecute(Runnable, Throwable)
我们在这里定制一个可以暂停和重新恢复的线程池
public class PausableThreadPoolExecutor extends ThreadPoolExecutor {
private boolean isPaused;
private ReentrantLock pauseLock = new ReentrantLock();
private Condition unpaused = pauseLock.newCondition();
public PausableThreadPoolExecutor(···) {
super(···);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
pauseLock.lock();
try {
while (isPaused)
unpaused.await();
}catch (InterruptedException ie){
t.interrupt();
}finally {
pauseLock.unlock();
}
}
public void pause(){
pauseLock.unlock();
try {
isPaused = true;
}finally {
pauseLock.unlock();
}
}
public void resume(){
pauseLock.lock();
try {
isPaused = false;
unpaused.signalAll();
}finally {
pauseLock.unlock();
}
}
}
线程池的状态
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;
此状态下,所有的任务都已经终结,工作线程数为0,并且会调用terminated()
勾子方法
private static final int TERMINATED = 3 << COUNT_BITS;
此状态下,terminated()
方法已经完成
内部类Worker的实现
类Worker主要是为了维护运行任务的线程的中断控制状态,以及一些其他的次要的记录。
final Thread thread; 记录这个worker运行的线程
Runnable firstTask; 初始化运行的任务
volatile long completedTasks; 完成的任务数
Worker的构造方法需要传递一个Runnable firstTask
来初始化
当worker执行run方法时,会将程序的逻辑委托给外部的runWorker
方法,那么runWorker
的运行流程是什么样的呢?
- 主逻辑是一个循环,从队列中获取
task
,如果task != null
就执行循环里的逻辑
- 这里介绍下从队列中获取
task
的方法——getTask()
这个方法是从任务队列中获取任务,在等待任务的过程中,可能会超时,可能会不停地阻塞,这个取决去线程池的配置。当然也可能由于worker必须得退出,导致获取的任务为空,在以下几种情况时,woker必须得退出。(1)池里面有超过最大线程数的worker存在,在程序运行过程中,调用setMaximumPoolSize
方法时,就极有可能出现此种情况;(2)当前池处于stopped状态;(3)当前池处于shut down
状态,并且任务队列为空;(4)当前worker获取任务超时,这是由于我们设置了allowCoreThreadTimeOut
或者当前worker数大于核心线程数。
- 获取到非null的task后,会依次执行
beforeExecute(wt, task);
、task.run();
、afterExecute(task, thrown);
execute(Runnable command)方法
分为三部曲
- 如果少于核心线程数的线程在运行,那么就新建一个worker,并把command当作第一个task
- 如果线程池在运行,并且任务队列能接收command,那我们就将command添加进任务队列,接着进行判断,是否需要新添加worker或者拒绝当前的command
- 直接添加worker,同样将command当作第一个task,如果失败,就拒绝当前的command
拒绝策略
CallerRunsPolicy
task的执行由调用者执行,也就是由执行execute(Runnable command)
的线程执行,前提是线程池没有shut down
,否则,该任务会被丢弃。
AbortPolicy
直接抛出异常
DiscardPolicy
直接丢弃任务,不做出任何的提示
DiscardOldestPolicy
丢弃任务队列中的第一个任务(也就是加入队列时间最久的任务),然后重新执行execute(command)
方法