需要开启新线程执行异步代码的时候通常都是new Thread()创建新的线程对象,创建线程过程中会向系统申请资源,造成任务启动变慢,直接开启的线程没有统一的管理机构,如果用户不断的创建新线程也没办法控制最多启动的线程数;直接开启的线程运行结束后还要做垃圾回收操作,影响整个应用的性能。
针对这些问题Java类库中提供了线程池功能,在线程池中会预先分配一些启动好的线程对象,当用户有任务要执行时向线程池提交任务,线程池会选择一个空闲的线程执行任务,任务结束后线程重新进入空闲状态等待下一次提交的新任务。线程池中的线程已经启动过,再提交任务的时候任务启动速度更快;用户向线程池提交任务如果任务太多线程池不会启动特别多的线程,而是对任务做排队处理,只有队列无法放下新的任务才会执行拒绝策略;任务运行完成后线程也不会被要求立即回收减少系统中的垃圾回收次数。
虽然Java类库已经提供了线程池的实现,为了学习线程池的具体原理有必要自己实现一个简单的线程池。首先考虑的是任务队列,线程池的任务队列主要用来在核心线程都在处理任务的时候无法接收新任务时缓存任务,当线程池里的线程有空闲状态的就会从任务队列里取任务执行。这样看来任务队列和生产者消费者中的产品队列类似,用户属于任务的生产者而线程池属于任务的消费者。
// 线程池任务队列
public class JobQueue {
private final Object mLock = new Object(); // 内部锁对象
private LinkedList<Runnable> mJobs = new LinkedList<>(); // 保存任务双向链表链表
private int mMaxJobs; // 最大任务数
public JobQueue(int maxJobs) {
this.mMaxJobs = maxJobs;
}
// 任务队列增加新任务
public boolean add(Runnable runnable) {
synchronized (mLock) {
// 队列满无法添加任务的时候不做等待,
// 需要启动普通任务线程执行或执行丢弃策略
if (mJobs.size() >= mMaxJobs) {
return false;
}
mJobs.addFirst(runnable);
mLock.notify(); // 通知有新任务进入
return true;
}
}
// 普通任务线程获取任务接口
public Runnable remove(long timeout) {
synchronized (mLock) {
long start = System.currentTimeMillis();
while (mJobs.isEmpty()) { // 如果任务队列为空,等待
try {
// 已经等待的时间
long waitTime = System.currentTimeMillis() - start;
// 伪唤醒导致wait()返回,等待时间没达到timeout,继续等待
if (waitTime < timeout) {
waitTime = timeout - waitTime;
} else {
// 如果已经到了超时时间,依然没有任务,
// 不再等待返回空对象
return null;
}
mLock.wait(waitTime);
} catch (InterruptedException e) {
// e.printStackTrace();
// 如果wait期间收到中断异常,用户关闭了线程池
if (mJobs.isEmpty()) {
return null;
}
}
}
return mJobs.removeLast();
}
}
// 核心线程获取任务接口
public Runnable remove() {
synchronized (mLock) {
while (mJobs.isEmpty()) {
try {
mLock.wait();
} catch (InterruptedException e) {
// e.printStackTrace();
// 如果wait期间收到中断异常,用户关闭了线程池
if (mJobs.isEmpty()) {
return null;
}
}
}
return mJobs.removeLast();
}
}
}
代码中定义了类似于产品队列的线程池任务队列,该队列内部有一个LinkedList类型的列表对象保存用户添加的任务对象,LinkedList集合本身并不是线程安全的,在多线程访问情况下操作时会出现数据异常,为此专门为它生成了内部锁对象lock,所有针对LinkedList操作都需要先获取锁对象。队列还有一个最大任务数,防止用户添加过多的任务导致占用过多内存。在产品队列中如果队列已满就需要生产者等待消费者消费产品后空出位置通知生产者继续生产,但任务队列不能如此实现,如果任务队列已满要求提交任务等待就会导致提交任务的线程无法继续工作,因而任务队列在队列满时返回false告知外部无法加入新任务,线程池可以根据策略增加新处理线程或者直接拒绝新任务。
任务队列实现完成后开始工作线程的实现,工作线程分为两种:核心工作线程和普通工作线程。核心工作线程一旦启动就和线程池的生命周期保持一致,线程池被关闭核心工作线程才会终止退出。想要保证核心工作线程即使没有任务也能够存活,需要核心工作线程在无任务时执行无限等待;普通工作线程在没有任务时会保活一段时间,也就是暂停等待一段时间,等到等待时间结束还没有新任务就直接退出。在第一节中讨论到线程暂停等待可以使用wait()/wait(timeout)来实现,在任务队列中获取任务时就会执行lock锁对象的等待方法。JobQueue的remove()方法内部调用wait()实现无限期等待,remove(timeout)则调用wait(timeout)执行有限时间内等待,不过wait()方法可能会被中断或伪唤醒打断每次从wait(timeout)返回时都需要重新计算新的超时时间。
下面的代码展示了核心工作线程内部的执行逻辑,它会有一个初始任务,在执行完初始任务后再从任务队列中获取新任务。线程池在接收到新任务时会先检查核心线程是否已经达到最大值,如果没有就开启新的核心线程,因为新任务进入任务队列还需要执行各种锁同步操作,线程池会将传递来的任务直接扔给新开启的核心工作线程执行,减少冗余的步骤。当然如果添加新任务时核心线程数已满,新任务就会被放到任务队列中共工作线程后面获取执行。
// 核心线程执行逻辑
public class CoreWorkerTask implements Runnable {
private JobQueue mJobQueue;
private MyThreadPool mThreadPool;
private Runnable mFirstTask;
@Override
public void run() {
if (mFirstTask != null) {
mFirstTask.run(); // 执行初始任务
mFirstTask = null;
}
while (mThreadPool.isOpen() || mThreadPool.isShutdown()) {
Runnable job = mJobQueue.remove(); // 内部执行的wait()
if (job != null) {
job.run(); // 执行任务队列中任务
if (!mThreadPool.isOpen() && mJobQueue.isEmpty()) {
break;
}
} else {
break;
}
}
}
}
普通工作线程则是在用户设置了最大线程数大于核心线程数,当核心工作线程已经满了,任务队列也已经充满了用户提交的任务,用户继续提交任务就会创建普通工作线程来处理。如果普通工作线程和核心工作线程的数量达到最大,用户再提交新任务就会执行拒绝策略;普通工作线程处理完分配的任务后还会继续从任务队列中取新的任务,如果此时任务队列为空普通工作线程会在用户设置的最大保留时间过后自动终止退出。
// 普通线程执行逻辑
public class WorkerTask implements Runnable {
private JobQueue mJobQueue;
private MyThreadPool mThreadPool;
private int mKeepTime;
private TimeUnit mTimeUnit;
private Runnable mFirstTask;
@Override
public void run() {
if (mFirstTask != null) {
mFirstTask.run();// 执行初始任务
mFirstTask = null;
}
while (mThreadPool.isOpen() || mThreadPool.isShutdown()) {
long waitTime = mTimeUnit.toMillis(mKeepTime);
Runnable job = mJobQueue.remove(waitTime); // 内部执行的wait(timeout)
if (job != null) {
job.run(); // 执行任务队列中任务
if (!mThreadPool.isOpen() && mJobQueue.isEmpty()) {
break;
}
} else {
synchronized (mThreadPool.mThreads) {
mThreadPool.mThreads.remove(Thread.currentThread());
}
break;
}
}
}
}
从工作线程的执行中可以看到它们都会有一个初始的任务,该任务是在工作线程被创建的时候分配的,普通工作线程初始任务的逻辑和核心工作线程是相同的,两者唯一的区别在于只要用户不关闭线程池核心工作线程始终都不会退出,而普通工作线程会在保活时间之后自动退出。
工作队列和工作线程设计完成之后开始考虑线程池对象将它们组合起来,线程池对象需要记录核心线程数量,最大线程数量,普通工作线程保存时长和时长单位,工作队列的最大容量,拒绝策略对象,线程池当前工作状态,线程池中活动的工作线程引用;还要提供用户提交任务的接口,关闭线程池的接口以及获取线程池当前工作状态的接口。线程池的工作状态分为三种,开启状态,关闭状态和立即关闭状态,关闭状态下会在执行完任务队列里的所有任务之后才终止所有的工作线程,而立即关闭即使工作队列中还有任务,工作线程依然会立即终止退出,用户任务只有在开启状态才能提交到线程池中,关闭和立即关闭都不能再继续向线程池中提交新任务。
// 线程池实现代码
public class MyThreadPool {
private volatile PoolState mState = OPEN; // 线程池状态
private JobQueue mJobQueue; // 任务队列
final Set<Thread> mThreads = new HashSet<>(); // 工作线程引用
private int mCoreThreadCount; // 核心工作线程数
private int mMaxThreadCount; // 最大工作线程数
private TimeUnit mTimeUnit; // 普通工作线程无任务时存留时间单位
private int mKeepTime; // 普通工作线程无任务时存留时间
private MyThreadFactory mThreadFactory; // 线程工厂
private int mMaxPendingJobs = Integer.MAX_VALUE;
private RejectPolicy mRejectPolicy; // 拒绝策略
// 上面所有的线程池配置都可以通过用户设置
public MyThreadPool(int coreThreadCount, int keepCount, int keepTime, TimeUnit timeUnit,
MyThreadFactory threadFactory, int maxPendingJobs,
RejectPolicy rejectPolicy) {
this.mCoreThreadCount = coreThreadCount;
this.mMaxThreadCount = keepCount;
this.mTimeUnit = timeUnit;
this.mKeepTime = keepTime;
this.mThreadFactory = threadFactory;
this.mMaxPendingJobs = maxPendingJobs;
this.mRejectPolicy = rejectPolicy;
this.mJobQueue = new JobQueue(maxPendingJobs);
}
// 添加新的工作线程,core是否是核心工作线程,firstTask初始执行任务
private void addThread(boolean core, Runnable firstTask) {
Thread thread = mThreadFactory.newThread(core ?
new CoreWorkerTask(this, mJobQueue, firstTask) :
new WorkerTask(this, mJobQueue, mKeepTime, mTimeUnit, firstTask));
final Thread.UncaughtExceptionHandler handler =
thread.getUncaughtExceptionHandler();
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
// 工作线程执行用户任务抛出异常,该工作线程终止清除引用
synchronized (mThreads) {
mThreads.remove(thread);
}
handler.uncaughtException(thread, throwable); // 执行默认的异常处理
}
});
mThreads.add(thread);
thread.start(); // 开启工作线程
}
public boolean isOpen() {
return mState == OPEN;
}
public boolean isShutdown() {
return mState == SHUTDOWN;
}
public boolean isShutdownNow() {
return mState == SHUTDOWN_NOW;
}
// 详细过程请参考图1-7
public void submit(Runnable runnable) {
if (!isOpen()) {
return;
}
synchronized (mThreads) {
// 如果核心线程未满,添加核心线程
if (mThreads.size() < mCoreThreadCount) {
addThread(true, runnable);
return;
}
}
// 核心线程已满,加入任务队列
if (mJobQueue.add(runnable)) {
return;
}
// 任务队列已满,无法加入,判断普通工作线程是否已满
synchronized (mThreads) {
if (mThreads.size() < mMaxThreadCount) {
// 启动普通工作线程处理任务
addThread(false, runnable);
} else {
// 任务队列和普通工作线程都已满,执行拒绝策略
mRejectPolicy.reject(runnable);
}
}
}
public void shutdown() { // 关闭线程池
this.mState = SHUTDOWN;
interruptThreads(); // 中断等待中的线程
}
public void shutdownNow() { // 关闭线程池
this.mState = SHUTDOWN_NOW;
interruptThreads(); // 中断等待中的线程
}
private void interruptThreads() {
synchronized (mThreads) {
for (Thread thread : mThreads) {
thread.interrupt(); // 调用中断方法打断wait()中的线程
}
mThreads.clear();
}
}
}
在submit()方法中用户提交自己的任务给线程池对象,线程池会先判定核心工作线程是否已经满,未满就添加新的核心工作线程并把此次提交的任务做为该核心工作线程的初始任务。用户提交的任务运行时有可能会抛出运行时异常,抛出的异常线程池通常不应该处理,而应该抛出给用户处理,默认情况下抛出异常会导致运行它的工作线程异常退出,异常退出的工作线程就不能再运行新的用户任务需要从工作线程集合中删除。addThread()方法负责向线程池中添加工作线程,它内部创建的新线程都会设置新的异常处理器,新异常处理器会先将当前线程从线程池中移除,在执行默认的异常处理结束当前线程执行。下一次用户提交新的任务时线程池会检查核心工作线程未满就会启动新的工作线程,这样就弥补了之前抛出异常终止退出的工作线程。
用户继续调用submit()增加新任务就会逐渐把核心工作线程都填满,再提交新任务就会被放到任务队列中,如果任务队列已满会告知线程池对象无法添加,线程判定普通工作线程是否已满,未满就继续执行addThread()添加普通工作线程,执行的流程和核心工作线程类似。不断添加新任务会导致线程池内部的核心线程、工作队列和普通工作线程全部饱和,如果还有新任务加入线程池就只能执行拒绝策略,当前的线程池工作已经完全饱和无法再处理任何新任务。
线程池状态是要在其他工作线程中访问的,如果用户调用了关闭线程池方法工作线程应该能够读取到最新的线程池状态,因而状态变量是有volatile关键字修饰的,保证它在多线程的内存可见性。在shutdown()/shutdownNow()方法中可以看到最终会依次调用线程池中每个工作线程的interrupt()方法,在第一节中暂停和中断中提到interrupt()方法能够打断处于wait()状态的线程,工作线程在没有任务时通过wait()暂停执行保持在内存中,用户关闭后它们接收中断并且抛出InterruptedException,工作线程内部就会将自己推出run()方法执行保证Thread成功退出执行。SHUTDOWN/ SHUTDOWN_NOW状态的线程池工作线程的退出方式会有所不同,如果是SHUTDOWN关闭状态就不会立即退出,而是不断从任务队列中获取已经提交的任务并执行,等到工作队列为空才退出;如果是SHUTDOWN_NOW立即关闭状态,不再判断工作队列是否为空直接退出即可。
自定义线程池测试Demo