网上介绍线程池的文章很多,质量好坏不一。能讲的很透彻的,确实不多。
本人能力有限,本文先从原理入手,讲清楚线程池是怎么运行的。
至于源码的分析,将单独写一篇(《线程池源码详解》)。
全文以 java 1.8 来说明。
一、示例代码
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
for(int i =1; i <= 10; i++){
int index = i;
pool.execute(new Runnable() {
@Override
public void run() {
String currentThreadName = Thread.currentThread().getName();
log.info("第{}次任务结束,执行者:{}", index, currentThreadName);
}
});
}
pool.shutdown();
System.out.println("All thread is over");
}
这个小例子中,执行了10次 run() 方法,但其实只起了两个线程。
这就是线程池的一个特点:节省资源
线程的创建与销毁是非常耗能的操作,若执行一个任务就起一个线程,那是消耗大了去了。
二、线程池的创建
示例中,Executors.newFixedThreadPool(2)
,最终是执行的下面这个方法(是ThreadPoolExecutor
这个类中的方法)
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
这里面有7个参数,等介绍源码的时候再详细说。这里只说涉及本本篇的三个:
corePoolSize
核心线程数maximumPoolSize
最大线程数workQueue
阻塞队列
顺便说一句,在线程池里,没有什么所谓的核心线程、非核心线程。
有些文章里说核心线程怎么怎么样
,非核心线程怎么怎么样
——瞎掰!
线程池创建出来的线程,地位是一样的,功能也是一样的。
阻塞队列,之前的博客有详细的讲解,这里就再展开。
线程的执行
execute(Runnable command) 这个方法是核心方法。
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)) { // 2、将任务放入阻塞队列
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false)) // 3、队满时,继续创建线程,直至工作线程数量达到最大值。
reject(command); // 4、执行拒绝策略
}
我写了4个注释,这是execute方法的执行逻辑。这里我举个通俗的例子,来说明。
假如病毒流行,感染的病人被领到医院看病。这里的病人相当于需要执行的任务。
医院里总共有 6 套超级防护服,医生需穿上防护服才可以给病人看病。
医院里拿出4套防护服供医生使用,留下两套备用。
这里的 6 相当于 maximumPoolSize 最大线程数,4 相当于corePoolSize
如果医生都在忙,那病人就被安置到大厅,排队看医生。这里的大厅相当于workQueue
上面是一个空图,现在来了一个病人,医生穿上防护服,给病人看病。
代码层面,就是执行 addWorker(command, true)
这一行。
之后又来俩病人。那就继续创建线程,同时呢,假设第一个来的病人,已经看完了,离开了诊疗室。
现在呢,假如一下子来了三个病人,
那先新上一个医生看病,第二、三个病人到大厅等着。
其实空闲的那个医生,会到大厅里叫病人的
顺便说一句:当未达到核心线程数时,就先创建线程,不管其它线程是不是闲着。
之后再来的病人,就进大厅等着。相当于执行这行代码 workQueue.offer(command)
。
顺便说一句:医生不会闲着,看完一个,就从大厅叫一个进来。
如果病人来的又多又快,四个医生都没闲着,大厅也满了,那就把备用的两个防护服给用上。
相当于执行这行代码 addWorker(command, false)
。
现在是六个医生全力工作,看完一个病人,就从大厅领进来一个病人处理。
如果即使是这样,病人来是源源不断的来医院,这时就进行拒绝策略。
相当于执行这行代码 reject(command)
。
循环取任务
addWorker(Runnable firstTask, boolean core)
这个方法是创建线程并会启动线程。
最终会调用 Thread 类中 start()
方法。JVM 会自在合适的时候,调用 线程的 run()
方法。
如果面试官问——JVM 怎么调用 run 方法的,机制是什么,合适的时候是什么时候
我不知道,已经脱离java源码的范畴了,你想知道,自己看hotSpot 的源码吧
在 ThreadPoolExecutor
类中,重写了 run()
方法
public void run() {
runWorker(this);
}
runWork() 这个方法,简化如下。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock();
try {
while (task != null || (task = getTask()) != null) {
w.lock();
try {
task.run();
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
} finally {
processWorkerExit(w, completedAbruptly);
}
}
这个方法中, task.run() 会调用示例代码中 重写的那个run() 方法,也就是执行任务了。
仔细看,任务是从 getTask()
方法中取出来的,是循环执行,while 循环
前面例子中,医生看完一个病人,会从大厅里叫一个新病人。
当一个医生从大厅里叫病人,比如叫了两分钟,都没叫到。
那就认为没病人了,会考虑要不要脱了防护服,回去休息。
要不要撤的依据是,诊疗室里的医生有几个,即与 corePoolSize
比较
如果大于4个,那就可以回去消息,否则就在诊疗室待命。
也就是说,没有病人的时候,会蜕变成上图的样子,直到一个病人与没有了,四个医生在待命。
取任务的方法,代码简化如下
private Runnable getTask() {
for (;;) {
int wc = workerCountOf(c);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
} catch (InterruptedException retry) {
}
}
}
这个方法本质是从队列中取任务,取出来后,在 runWork() 方法里执行。
那最忙的时候,是六个医生在工作,之后不忙了,缩减到4个,代码层面体现在哪里呢?
看 runWork()
方法,在finally里执行 processWorkerExit()
这个方法。
能走到这个方法,意味着大厅里没有病人了,可以会缩减医生。
简化后的代码如下:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w); // 缩减线程
} finally {
mainLock.unlock();
}
tryTerminate();
}
workers.remove(w)
这个就是将工作线程给剔除,GC回收。
线程关闭
在示例代码中,调用 shutdown()
方法,会关闭线程。代码如下,这个不用简化,很简洁
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess(); // 检查有没有权限关闭线程池
advanceRunState(SHUTDOWN); // 更改线程池的状态
interruptIdleWorkers(); // 中断线程
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
等到介绍源码的时候,再细说如何优雅的关闭线程池。
总结
梳理下,本篇简单介绍了,线程池的创建、运行、关闭。
用画图的方式,介绍了,线程池工作原理,即 execute() 方法的四大步
- 接到任务,先创建工作线程
- 工作线程达到一定数量,把任务往阻塞队列放
- 阻塞队列放不下了,创建工作线程(不超过最大线程数)
- 如果前三步应付不过来,执行拒绝策略
线程工作,即 addWorker()
方法执行里,调用 Thread
类的 start()
方法,JVM 会调用 run()
方法
重写的 run()
方法执行 runWorker()
方法,此方法会循环取任务,直到没有任务了。
随后会维护线程池里线程的数量。最后是关闭线程池。
看了之后,有没有一种错觉,线程池的代码挺简单的呀,没有什么复杂的逻辑呀!
事实是代码巨复杂,这篇文章是把代码简化了,只说大流程。
有了这篇的基础,下篇解析源码时,会详细说明
- 线程池状态是怎么控制的
- 创建线程是怎么防止并发的
- 核心线程数和最大线程数,具体干啥的
- 哪些地方用了 AQS 框架
- 线程最长存活时间,是怎样控制线程生命周期的
- addWorker 时,传一个空任务,是干嘛的
- 常用线程池,是怎么选阻塞队列的
- ……
所有这些,请看下篇《线程池源码详解》。