1.前言
线程池创建一定数量的线程,并加到池中等待工作,这种池化技术可以复用我们的线程,并且还能根据不同的策略去执行任务,这篇文章主要探讨下线程池里的线程如何运行,以及如何复用线程的
2.任务的执行
先看下如何将我们的任务提交给线程池
public static void main(String[] args){
//通过工厂类创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
for (int i = 0;i < 5;i++){
int finalI = i;
//提交任务
executorService.submit(()->{
System.out.println(Thread.currentThread().getName()+"完成任务" + finalI );
});
}
}
运行结果如下
通过调用线程池对象的submit方法就可以把我们的任务提交上去,并且从结果可以看出,由线程池的两个线程完成我们的任务,这样就实现了线程的复用
线程池的submit方法是有返回值的,其返回值是Future类,如果我们submit的参数里传入的任务是有返回值的,那么当任务执行完毕,返回的Future类调用get方法可以得到我们的返回值,这里不作赘述
3.任务的封装
下面我们来看下submit之后,我们的任务经历了哪些过程(下面以ThreadPoolExecutor作介绍)
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
//将任务封装
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
可以看到第一步是将我们的任务进行封装,这里我们先不关心封装的类是什么,因为在execute方法中是把我们的ftask当作Runnable接口来使用的,下面看下execute方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* 这里有一段java官方写的英文注释,描述了后面代码执行的逻辑
*/
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);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
可以看到在添加任务前,会先判断当前线程池核心线程数量是否达到最大值,如果未达到最大值,则创建一个核心线程去执行。
如果达到最大值。首先会去查看线程池运行的状态(isRunning查看线程池是否是运行状态),然后尝试加入等待队列。如果成功加入等待队列,那么会再次判断线程运行的状态,如果发现此时线程池关闭,则会移出刚才添加的任务,并执行拒绝策略。如果线程池处于运行状态,但是发现没有工作线程(workerCountOf返回工作线程数),则创建非核心线程去执行
最后如果无法加入等待队列,则尝试创建非核心线程去执行,如果失败则执行拒绝策略
经过这一系列的判断后我们的任务才能开始被执行,下面来看下addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
//这里有一段判断运行状态的代码...
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//封装worker类
w = new Worker(firstTask);
//实际执行的线程
final Thread t = w.thread;
if (t != null) {
//这里会有个加锁的操作
try {
//有一段判断状态的代码
//如果判断都通过则我们创建出来的worker会被加入我们的workers集合中
} finally {
//释放锁
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
(上面我忽略的比较多的代码,每段代码大意已经用注释形式给出)
可以发现我们传入的任务被传入到一个worker对象,worker类是线程池的一个内部类,是对线程的一个封装,worker实现了Runnable接口,所以其本身就有run方法,并且内部维护了一个Thread类。
可以看到最后调用了start方法来启动我们的线程,到这里我们的任务就封装好,可以看到中间经过了层层判断,只能说java的设计师太 ‘细’ 了
4.Worker
前面说过worker类实现了Runnable接口,所以其实worker本身就作为一个任务来执行,而其维护的Thread调用的也是worker的run方法
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
下面来看下我们的worker类是如何运行的
public void run() {
runWorker(this);
}
可以发现worker的run方法调用的是runWorker这个方法,并且传入了其本身,来看下这个方法是如何运行的
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
//省略部分代码...
try {
while (task != null || (task = getTask()) != null) {
//一段判断的代码...
try {
//...
try {
task.run();
}
// ...
} finally {
task = null;
w.completedTasks++;
//...
}
}
//...
} finally {
processWorkerExit(w, completedAbruptly);
}
}
(在上面我省略了比较多的代码段,主要是判断和一些加锁的操作)
可以发现实际上执行的是一个while循环,这个循环会不断的去获取任务,然后执行,这里的getTask方法就是去任务队列中获取任务我们的线程会阻塞在这个位置,下面来看下getTask是如何去获取任务的
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
//省略部分代码...
//判断是否要线程收缩/销毁非核心线程
int wc = workerCountOf(c);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
(上面省略了部分代码)
可以发现是通过workQueue的poll或者take方法来获取的,poll方法会传入一个等待时间,如果等待时间到没有返回,就会执行下面的timedOut = true,在下次判断就会退出这个方法,导致外层得到的是null,最后退出while循环,take方法则一直阻塞等待,具体阻塞方式由实现类实现(比如通过锁之类的)
5.总结
以上就是本人对线程池复用线程的一个理解,创建出来的线程会在一个while循环中不断的尝试获取任务队列里的任务,直到没有任务