前言
在Java语言中线程池是一个非常有用的Api同时也是一个非常有意思的设计,随着搬砖的时间的积累以及头发的变少慢慢的也对各种源代码也越来越感兴趣了。之前也看过网上很多对线程分析和讲解的文章,但是总是记不住他们到底讲了什么,真正等到来用的时候又忘记了,其实只有我们真正的钻入到代码以后才能真正的理解,然后再加以用我们生活的一些例子来对里面的术语进行形象化。
池化技术
程序的运行,其本质上是对系统资源(CPU,内存,磁盘,网络等等)的使用,如何高效合理的使用这些资源是我们编程所要考虑的一个问题,线程池其实就是对线程的一个管理的工具,使其能达到最大的优化效果。除了我们所说的线程池,还有连接池,内存池,对象池等等,其思想都是一样的。
线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池进行统一分配,调优和监控,可以达到下面的好处:
- 降低资源消耗
- 提高响应的速度
- 提高线程池的可管理性
使用案例
public class ThreadTest {
public static void main(String[] args) throws Exception {
ExecutorService executorService = new ThreadPoolExecutor(2, 10,
60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("task running");
}
});
}
}
类结构
案例
其实很多时候我们可以将编程中的东西进行形象化,或者是将生活中的示例进行抽象到编程中使其形成模型。
我们将 ThreadPoolExecutor 比作是工厂
- corePoolSize 核心线程数量(比作工厂正式员工)
- maximumPoolSize 最大线程数量(比如工厂最多能接受多少员工,总的员工减去正式的就是临时工)
- keepAliveTime 保持活的时间长短(表示临时工没事做的情况下待在厂里的时间)
- allowCoreThreadTimeOut 核心线程是否会超时 (表示是否运行辞退正式员工)
- unit 时间单位,比如说年、月、日、时、分、秒
- threadFactory 创建线程的工厂(比作是公司的招聘人员,比如 HR 之类的)
- Thread 线程(比作是员工)
- Runnable 运行的任务(比作是工厂里的订单或者是事情)
- handler 线程池拒绝策略(当工厂的订单已经达到了上限以后,我们需要拒绝外来的订单,这就是拒绝策略)
- workQueue 工作队列用于存放未执行的任务(当工厂的订单太多的话)
首先在创建对象的时候我们需要指定具体的参数,注意的我们在创建对象的时候其实是什么都没有创建的,只是简简单单的传入了一些参数进去而已,只有我们调用了execute方法以后才会真正的创建线程的。
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
........
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
提交的任务
public void execute(Runnable command) {
int c = ctl.get();
//首先获取工作线程数目是否小于我们规定的大小
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);
} else if (!addWorker(command, false))
//这里表示等待队列已经存放不下任务,并且核心线程已经完全占用了。直接调用拒绝方法处理
reject(command);
}
上面的代码我们可以总结为三种情况:1、当工作的核心线程数小于我们规定的数目的话,则直接执行任务;2、当核心线程的个数大于或者等于规定的数目的话,这个时候的任务会添加到队列中保存起来,待其他的任务执行完成以后再去队列中获取任务;3、当核心线程个数超过规定时,等待队列中的任务也已经满了的话,如果继续添加任务则会遭到拒绝(直接抛异常)。
执行任务 addWorker
- 首先就是不断循环的判断线程池的状态、核心线程个数、总的核心线程个数是否超过我们规定的个数
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
//获取运行状态值
int rs = runStateOf(c);
//如果状态是shutdown 而且队列不为空的话,那么直接不增加
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
//如果核心线程数目操作规定大小,或者是总的线程个数超过规定大小的话,都不让添加任务了
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//同时将工作线程个数 + 1
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
}
}
.........
}
当我们把这些条件都判断完成以后就会创建一个 Worker 对象出来,然后将task(Runnable)封装到Worker中
private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
final Thread thread;
Runnable firstTask;
//线程执行的任务总数
volatile long completedTasks;
//在创建Worker对象的同时会创建一个Thread对象,最后启动thread话调用的方法是runWorker方法
Worker(Runnable firstTask) {
setState(-1);
//将需要执行的 Task传进来
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
}
所有的信息都封装到Worker以后就需要对Worker进行执行了。
public boolean addWorker(Runnable firstTask, boolean core) {
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
int rs = runStateOf(ctl.get());
//这里再次检测线程池的状态
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
//如果 worker 已经启动的话,直接抛出异常
if (t.isAlive())
throw new IllegalThreadStateException();
//由于HashSet不是线程安全,所以在 add时候需要加锁操作
workers.add(w);
int s = workers.size();
//修改 largestPoolSize的值
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
//直接释放锁
mainLock.unlock();
}
如果之前添加成功直接启动Worker中的线程,也就是执行 runWorker方法
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
//如果 worker启动失败的话,则从workers中移除
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
Worker启动失败的话需要将worker从HashSet中移除,同时工作线程个数减 1
private void addWorkerFailed(Worker w) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//从HashSet中移除Worker
if (w != null)
workers.remove(w);
//将工作线程的个数减 1
decrementWorkerCount();
//尝试去终止
tryTerminate();
} finally {
mainLock.unlock();
}
}
上面的代码非常的简单仅仅只是做了一些判断,然后将Worker保存到一个HashSet中,如果添加成功的话则启动Worker中的线程,最后也就是执行 runWorker方法的,该方法才是真正核心的执行方法。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
boolean completedAbruptly = true;
try {
//不断循环的获取 task,来源可能是来自于 Worker中,也有可能来自于阻塞队列中
while (task != null || (task = getTask()) != null) {
w.lock();
//如果线程池状态是stop的话,则要将线程中断
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//执行任务前的操作,供外部去实现
beforeExecute(wt, task);
Throwable thrown = null;
try {
//执行Runnable接口的run方法,也就是执行任务。现在我们明白了
task.run();
} catch (Error x) {
thrown = x; throw x;
} finally {
//执行任务前的操作,供外部去实现
afterExecute(task, thrown);
}
} finally {
//最后释放锁,并且该线程完成的task数目+1,最后需要释放锁
task = null;
w.completedTasks++;
w.unlock();
}
}
//完整的执行任务以后 completedAbruptly 标志为false。
completedAbruptly = false;
} finally {
//如果是线程抛出的异常,则需要处理Worker线程退出的情况
processWorkerExit(w, completedAbruptly);
}
}
通过上面的代码我们就可以非常清晰的知道Worker内部就是封装了一个Thread,执行外部的任务Runnable的时候就是执行Runnable的run方法。我们通过不断循环来获取队列中的Task然后执行Task的代码,那getTask则是一个非常重要的方法。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//检查线程池的状态为停止状态,并且队列中也没有任务的话则直接返回 null
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
//获取运行的线程个数
int wc = workerCountOf(c);
//判断我们是否设置允许核心线程过期,以及核心线程个数超过规定数目
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
//如果工作的线程个数大于我们规定的上线的话,我们则需要销毁一些Worker
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
//这里直接返回null的话,Worker则不进行循环里面执行任务了
return null;
//再次循环检测线程个数是否超过我们规定的
continue;
}
try {
//如果我们设置了核心线程过期的话,则直接调动poll方法等待keeptime以后如果还没有任务的话则直接结束线程,否则直接调用take阻塞当前的线程直到有新的任务进来以后才会唤醒该线程的。
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
注意 上面是获取任务的关键代码,其实也是核心线程在没有任务的时候怎么处理以及临时线程超时如何处理,下面就分为两种情况解释:1、当线程池中总的工作线程超过了核心线程数但是小于规定的总线程数的话,如果从阻塞队列中没有任务的话则当前线程会阻塞keepAliveTime再进行获取一次任务,最后还是没有任务进来的话,最后才会进行销毁的。核心线程则不会销毁,如果队列中一直没有任务的话则会一直处于阻塞的状态直到队列中来了任务以后才获取任务的。2、如果我们设置允许核心线程过期,一样会走第一步的。
private void processWorkerExit(Worker w, boolean completedAbruptly) {
//如果线程因为中断而结束的话,则直接将工作线程数目减 1
if (completedAbruptly)
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//完成的任务个数+1,并且从HashSet中移除该线程
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
tryTerminate();
int c = ctl.get();
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
//如果Worker不是因为异常结束的话,则判断是否需要增加线程
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && !workQueue.isEmpty())
min = 1;
//如果工作线程个数大于规定的数目直接return
if (workerCountOf(c) >= min)
return;
}
//添加临时Worker(线程),也可以说增加临时工(注意这里的临时工是可以转正的)
addWorker(null, false);
}
}
当Worker中的线程最后执行完成我们需要处理一些数目的统计,比如更新工作线程个数、需要从HashSet中删除工作的Worker,有时候还需要加入临时的工作线程,这里就非常的容易理解了也没有什么复杂的操作以及绕口的逻辑了。
我们之前在构造ThreadPoolExecutor对象时并没有创造Worker(也就是说没有创建线程)只是传入了一些参数给成员变量的,只有真正的调用了execute 方法以后才会去创建 Worker对象的。下面我也可以创建线程池对象以后调用方法先创建出线程,然后再把任务放到Worker中去执行的。
- 创建单个工作线程(Worker)
public boolean prestartCoreThread() {
//首先会判断工作线程个数是否超过规定个数
return workerCountOf(ctl.get()) < corePoolSize &&
addWorker(null, true);
}
- 创建多个工作线程(Worker)
public int prestartAllCoreThreads() {
int n = 0;
//通过一个循环不断的去创建核心线程,如果其中创建失败了则退出循环,返回实际创建的个数
while (addWorker(null, true))
++n;
return n;
}
小结
通过上面我们对代码的分析可以很清楚的知道线程池的内部的原理与逻辑了,所谓的线程池无非就是一个池子然后里面存放着具体数量的核心线程,具体数量的临时线程。如果任务量过大的话,我们就将这些任务保存到内存中(可以是集合,数组,队列等等),当工作的线程执行完成任务以后再去内存中获取继续执行任务,但是如果内存中没有任务的话核心线程则会自己阻塞住自己(我们也可以立即为暂停的意思),如果是临时线程则会阻塞具体时间以后再看看有没有任务,如果还是没有任务的话那么该线程也算是执行完成结束了。其实这里面有几个非常重要的概念就是 阻塞队列,当队列中没有任务的时候,线程则会处于等待的状态。如果有任务的话则会唤醒线程去执行Runnable。这个也是线程池的不需要总是创建线程(Thread),销毁线程了,这些线程会一直在内存中占用资源的,这个也是我们规定真正运行的线程的个数。
生活类比
其实线程池的思想跟我们平时工厂里面工人的调度,任务的分配,以及仓库的存储能力是一样的道理,只是现在我们是对操作系统的资源的申请,对代码的执行以及调度的,其思想是一样的原理。比如说一个工厂容量有限最多只能容下 maximumPoolSize个员工,你可以规定多少个正式员工(corePoolSize),多少个临时员工。如果后面需要做的产品(任务)变多的话,我们可以先把产品放到仓库里面(workQueue),万一仓库里面也堆满了,然后正式员工也来不及做的话,那么就必须招聘临时工(临时线程)去做事情。最后当事情做完以后临时线程则会再等上一段时间(keepAliveTime)如果还没有任务来的话就会辞职(销毁)。但是正式员工则会一直等待事情的到来,如果没有事情的话,那么正式员工就处于等待状态(线程阻塞状态),当然我们可以开除正式员工(需要设置allowCoreThreadTimeOut 为true。也就是销毁核心线程)。如果核心员工被开除了下次再有任务进来的话,那么就需要人事HR去招人了(ThreadFactory 来创建线程)。
编程感悟
很久以前我们总是停留在如何使用别人的Api的脚步上,但是这样子对于我们真正的编程,如何将生活的例子或者是问题通过抽象化,模型化转换成计算机世界里面的事物没有帮助和效果的。其实上面的这些例子我们大家在生活中都懂的,但是为啥我们就不能设计出来这么好的编程工具呢?最后还一个就是不要一味的去看网上别人的博客,我觉得那是没有用的,因为那是别人的理解,而不是你的。最好的方法就是Read the fucking source code(阅读源代码)才能让你真正的提高。