为什么要使用线程池
为了尽可能的去压榨CPU,我们会在程序中使用多线程技术,这样在一些情景下会显著的减少CPU的闲置时间,增加CPU的吞吐能力。
假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。
在Java中,Doug Lea大神已经帮我们提供好了现成的线程池实现, java.util.concurrent.Executors类
提供了一个 java.util.concurrent.Executor
接口的实现用于创建线程池。
创建线程池的方法 | 线程池名称 | 线程池简单解释 |
---|---|---|
newFixedThreadPool | 固定大小线程池 | 该线程池中用来处理任务的最大线程数是固定的,一旦达到了限制,后续的任务就会在前面线程中的任务完成后复用线程来完成任务的执行。 |
newCacheThreadExecutor | 可缓存线程池 | 当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。 |
newScheduleThreadExecutor | 定时任务线程池 | 大小无限制的线程池,支持定时和周期性的执行线程 |
线程池原理
来看一个线程池使用示例。
public static void main(String[] args) throws InterruptedException {
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 50; i++) {
es.submit(() -> {
try {
int random = new Random().nextInt();
Date date = new Date();
if (random % 2 == 0) {
date = ConcurrentDateUtil.parse("2018-02-19 06:02:20");
}
if (!ConcurrentDateUtil.format(date).equals(DateUtil.formatDate(date))) {
System.out.println("证明非线程安全");
} else {
System.out.println("====");
}
// System.out.println(Thread.currentThread().getName() + "同步:" + date +
// "format:" + DateUtil.formatDate(date));
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
es.shutdown();
if (es.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("线程池已关闭");
} else {
System.out.println("线程池未正常关闭");
}
}
使用ExecutorService es = Executors.newFixedThreadPool(5);
来创建一个线程池。
使用es.sumbit()
来向线程池提交任务。
使用es.shutdown()
方法来尝试关闭线程池。
使用es.awaitTermination(10, TimeUnit.SECONDS)
来检测线程池是否正常关闭。
以上就是一个典型的线程池使用的示例。
-
线程池的架构
- 线程池的接口设计
public interface Executor { //可以说是线程池中最关键的方法 //顾名思义,执行 void execute(Runnable command); } //所有的线程池都实现了这个接口 public interface ExecutorService extends Executor { /* 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。 此时线程池不能够接受新的任务,它会等待所有任务执行完毕; 线程处于SHUTDOWN状态 */ void shutdown(); /* 线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务; 停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。 */ List<Runnable> shutdownNow(); /* 当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。 */ /* ==== */ /* 如果关闭后所有任务都已完成,则返回 true。注意,除非首先调用 shutdown 或 shutdownNow,否则 isTerminated 永不为 true。 */ boolean isTerminated(); /* * Blocks until all tasks have completed execution after a shutdown * request, or the timeout occurs, or the current thread is * interrupted, whichever happens first. 请求关闭、发生超时或者当前线程中断,无论哪一个首先发生之后,都将导致阻塞,直到所有任务完成执行。 */ boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; /* 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。该 Future 的 get 方法在成功完成时将会返回该任务的结果。 如果想立即阻塞任务的等待,则可以使用 result = exec.submit(aCallable).get(); 形式的构造。 ****** 注:Executors 类包括了一组方法,可以转换某些其他常见的类似于闭包的对象,例如,将 PrivilegedAction 转换为 Callable 形式,这样就可以提交它们了。 */ <T> Future<T> submit(Callable<T> task); /* 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功完成时将会返回给定的结果。 ****** */ <T> Future<T> submit(Runnable task, T result); /* 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功 完成时将会返回 null。 */ Future<?> submit(Runnable task); /* 执行给定的任务,当所有任务完成时,返回保持任务状态和结果的 Future 列表。返回列表的所有元素的 Future.isDone() 为 true。注意,可以正常地或通过抛出异常来终止已完成 任务。如果正在进行此操作时修改了给定的 collection,则此方法的结果是不确定的。 返回的Future列表与给出的任务列表顺序相同 */ <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; /* 执行给定的任务,当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表。返回列表的所有元素的 Future.isDone() 为 true。一旦返回后,即取消尚未完成的任务。注意,可以正常地或通过抛出异常来终止已完成 任务。如果此操作正在进行时修改了给定的 collection,则此方法的结果是不确定的。 */ <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException; /* 执行给定的任务,如果某个任务已成功完成(也就是未抛出异常),则返回其结果。一旦正常或异常返回后,则取消尚未完成的任务。如果此操作正在进行时修改了给定的 collection,则此方法的结果是不确定的。 */ <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException; <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
Executor接口是顶层接口,他只有一个方法,那就是提交任务的方法
execute(Runnable command)
,JUC中的线程池实现类都是直接实现的ExecutorService
接口,这个接口提供了很多线程池操作的方法定义。- 线程池的核心组件
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新 任务。
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
接下来我们来分析线程池实现的关键源码
先看最核心的方法
-
execute
ThreadExecutorPool
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
//如果线程数目小于线程池大小,则自己添加一个新线程
if (workerCountOf(c) < corePoolSize) {
//添加一个新线程成功
if (addWorker(command, true))
return;
c = ctl.get();
}
//如果线程池线程数已满,将Runnable任务添加到等待队列中
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//如果线程池状态不是运行状态(SHUTDOWN,STOP等)
//拒绝新任务加入
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//如果没有添加成功,就会尝试直接创建一个新的线程(CachePool就是这样的)
else if (!addWorker(command, false))
reject(command);
}
1.如果Worker线程数目小于线程池大小,则自己添加一个新Worker线程
1.1如果线程没有添加成功,则重新获取下ctl的值
2.如果线程池Worker线程数已满,且线程池的状态还是运行状态,将Runnable任务添加到等待队列中
2.1 如果线程池状态不是运行状态,则调用拒绝策略来处理任务
3.否则,直接尝试创建一个新的Worker线程,如果失败,则调用拒绝策略来处理任务
接着我们来看新增Worker线程的方法
-
addWorker
private boolean addWorker(Runnable firstTask, boolean core) { //retry是一个标志位 /* 其实retry就是一个标记,标记程序跳出循环的时候从哪里开始执行,功能类似于goto。retry一般都是跟随者for循环出现,第一个retry的下面一行就是for循环,而且第二个retry的前面一般是 continue或是 break。但是这个标记不一定非要命名为retry retry1 mytry等等都可以 Java中的label Java中label的出现改变了break只能跳出一层循环的局限性 */ retry: for (;;) { int c = ctl.get(); //获取当前线程池运行状态 int rs = runStateOf(c); //如果线程池的运行状态为SHUTDOWN,STOP等,返回false,添加任务失败,当状态为SHUTDOWN等时,不应该再向线程池添加任务 // Check if queue empty only if necessary. 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; //成功增加一个可创建线程的指标,使用label跳出外层循环 if (compareAndIncrementWorkerCount(c)) break retry; c = ctl.get(); // Re-read ctl if (runStateOf(c) != rs) continue retry; // else CAS failed due to workerCount change; retry inner loop } } //下面开始真正的开启线程 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 { // Recheck while holding lock. // Back out on ThreadFactory failure or if // shut down before lock acquired. int rs = runStateOf(ctl.get()); //再校验一次线程池的状态,因为之前的操作都是没有加锁的,可能会被其他线程更改掉当前线程池的状态。 if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) { if (t.isAlive()) // precheck that t is startable throw new IllegalThreadStateException(); //添加到Worker线程队列中 workers.add(w); int s = workers.size(); if (s > largestPoolSize) largestPoolSize = s; workerAdded = true; } } finally { mainLock.unlock(); } if (workerAdded) { //开启线程 t.start(); workerStarted = true; } } } finally { if (! workerStarted) addWorkerFailed(w); } return workerStarted; }
0.开启一个无限循环
1.获取当前线程池的状态,查看是否应该再向线程池添加任务,如果不该继续添加,则直接返回。
if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false;
第一个条件语句,如果线程池状态大于或等于SHUTDOWN
第二个条件语句,只要这个条件中的三个子条件有一个不满足的话,就会导致这个语句返回true。
所以就是当线程池状态大于或等于SHUTDOWN,除非此时线程池状态为SHUTDOWN且等待队列不是空的且当前任务不为空时,要不然就不允许再向线程池添加任务了。(这个在讲解线程池的关闭时会再次说到)
2.在内部再开启一个无限循环,跳出内部循环的条件
如果线程池Worker线程数已达到最大可创建线程数,就不会在创建新的线程了,直接return跳出循环。
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false;
这里最大可创建线程数与方法的参数
core
相关,如果该参数是true,那么最大可创建线程数为核心线程数大小,如果为false,则是最大线程数大小。如果成功增加一个可创建线程的指标,使用label跳出外层循环
如果线程池的状态改变了,回到外部循环,重新开始循环。
3.从2中,只有我们成功增加一个可创建线程的指标时,才会在跳出大无限循环的同时,不直接返回。接下来就是在成功创建一个Worker线程后,执行线程的逻辑了。
3.1使用当前任务为构造方法参数创建一个Worker对象。
3.2使用锁将新建的Worker添加到Worker线程队列中
3.3如果成功添加了,那就开启Worker线程。
从上述流程看,我们很明显能够知道Worker这个类是实现了Runnable接口的,当开启了线程之后,运行的就是Worker里面的run方法。
/** Delegates main run loop to outer runWorker */ public void run() { runWorker(this); } final void runWorker(Worker w) { Thread wt = Thread.currentThread(); Runnable task = w.firstTask; w.firstTask = null; w.unlock(); // allow interrupts boolean completedAbruptly = true; try { //不停的去拿workers队列中的Runnable任务来执行 while (task != null || (task = getTask()) != null) { w.lock(); // If pool is stopping, ensure thread is interrupted; // if not, ensure thread is not interrupted. This // requires a recheck in second case to deal with // shutdownNow race while clearing interrupt if ((runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP))) && !wt.isInterrupted()) wt.interrupt(); try { beforeExecute(wt, task); Throwable thrown = null; try { //执行任务的run()方法 task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { afterExecute(task, thrown); } } finally { task = null; w.completedTasks++; w.unlock(); } } completedAbruptly = false; } finally { processWorkerExit(w, completedAbruptly); } }
所以真正执行我们添加到线程的Runnable任务是在Worker这个ThreadPoolExecutor内部类的runTask方法中。
这个方法简单来说就是通过调用
getTask()
方法不断的去执行等待队列中的任务,达到任务复用线程的效果,直到方法为空。一旦方法返回为空,即退出循环,那么就会调用finally中的processWorkerExit
方法,该方法会将当前worker踢出Worker线程队列。当前线程也就跟着消亡了。
-
getTask
private Runnable getTask() { boolean timedOut = false; // Did the last poll() time out? for (;;) { int c = ctl.get(); int rs = runStateOf(c); // Check if queue empty only if necessary. //如果线程池当前的状态是SHUTDOWN以上,并且线程池已经STOP或者workQueue任务队列是空的情况下,才会 if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { //清空已经开启的工作线程 decrementWorkerCount(); return null; } int wc = workerCountOf(c); // Are workers subject to culling? boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) { //使用CAS来保证操作成功进行,不成功则继续循环一次 if (compareAndDecrementWorkerCount(c)) return null; continue; } try { /* poll() 获取并移除此队列的头部,在指定的等待时间前等待可用的元素(如果有必要)。 take() 阻塞 获取并移除此队列的头部,在元素变得可用之前一直等待(如果有必要)。 如果设置了超时时间 workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS); this.keepAliveTime = unit.toNanos(keepAliveTime); 会将线程池设置的超时时间转为NANOSECONDS 如果没有设置超时时间且当前线程数并没有大于corePoolSize核心池大小 那就调用workQueue.take();正是调用了workQueue.take()这个会无限阻塞的方法, 才使得没有设置超时时间的一些线程池中的线程是无限存活的。 */ Runnable r = timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); if (r != null) return r; timedOut = true; } catch (InterruptedException retry) { timedOut = false; } } }
0.开启一个f无限循环
1.查看线程池的状态,如果线程池当前的状态是SHUTDOWN以上,并且线程池已经STOP或者workQueue任 务队列是空的情况下,返回null,一旦返回null,也就意味着当前线程会走向消亡了。
2.查看当前线程池中Worker线程的个数,如果个数超过限制,减少一个Worker线程,返回null.
3.尝试获取等待队列中的任务,当没有设置超时时间的时候,会一直阻塞,直到获取当任务,所以对于没有设置超时时间的线程池来说,在不改变线程池状态时,Worker线程都是一直存活的。
如果有设置超时时间,当超过超时时间还没有获取到队列中的任务时(该过程没有被中断),那么也会因为满足2中的条件,返回null,然后这个Worker线程就会消亡。
所以,对于没有设置超时时间的线程池来说,在不改变线程池状态时或对线程发出中断请求时,Worker线程都是一直存活的。
而对于设置了超时时间的线程池来说,当超过超时时间还没有获取到队列中的任务时(该过程没有被中断),那么该Worker线程就会消亡。
几种ThreadPoolExecutor的总结
上面分析了线程池ThreadPoolExecutor的execute
方法,大致了解了下线程池处理线程的原理。
现在我们对几种常见的线程池来进行具体分析。
我们先来看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;
}
参数名 | 参数意义 |
---|---|
corePoolSize | 核心线程池大小 |
maximumPoolSize | 最大线程池大小 |
keepAliveTime | 超时时间,当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。(类似数据库超时时间) |
unit | 超时时间的单位 |
workQueue | 任务执行前用于保持任务的队列。此队列仅保持由 execute方法提交的 Runnable任务 |
threadFactory | 执行程序创建新线程时使用的工厂 |
handler | 拒绝策略,由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序 |
一般来说,不建议我们直接调用ThreadPoolExecutor的构造方法来新建一个线程池。而是调用Excuetors这个顶层的类来得到线程池实例。 下面要介绍的几种线程池就是通过使用不同的参数实现ThreadPoolExecutor
构造方法,来达到不同的对线程处理策略。
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
我们看到该构造方法的corePoolSize
和maximumPoolSize
是一样大的,没有超时时间,保存未执行的任务的队列为LinkedBlockingQueue
(默认大小为Integer.MAX_VALUE
,很多资料直接说该阻塞队列是无界队列),所以当addWorker
方法已经在线程池添加了corePoolSize
个线程之后,当有新的任务进来时,就会被加入到LinkedBlockingQueue这个阻塞队列中,等待着线程池中已有的线程通过getTask()
方法获取在workQueue
中的任务,因为没有超时时间,所以在getTask()
方法中,不会丢弃任务,而是使用workQueue.take()
来获取队列中的任务,会一直阻塞到获取到任务。
CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
他的核心线程池大小为0,但是最大线程数却没有限制。并且有一个60秒的超时,这意味着这个线程池中的工作线程不是无限存活的。关键的是他的任务队列使用的是SynchronousQueue这个同步阻塞队列,这个队列有一个很关键的特性(其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。同步队列没有任何内部容量,甚至连一个队列的容量都没有。)
当有新任务到来,任务插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
重新回到execute()
方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//1
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//2
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);
}
//3
else if (!addWorker(command, false))
reject(command);
}
当第一个任务进来的时候,前面两个分支条件都不符合,直接调用最后一一个分支的代码,开启一个Worker线程执行任务。
第二个任务进来,第一个条件分支是不符合的,因为corePoolSize
为0,所以尝试分支2,对于SynchronousQueue
队列,你想要添加成功,必须已经有一个线程在执行获取元素的操作才行,所以就意味着,如果想要添加成功,那么就必须要求,要有一个Worker线程执行完了任务,被阻塞在getTask()
方法中的获取等待队列中任务中,这样才能成功插入到等待队列中。这就要求任务的处理熟读要大于任务的提交速度了,要不然就满足不了第二个条件分支,调用最后一个分支的代码,又开启一个Worker线程。
所以该线程池适合如下特性的任务:
- 耗时较短的任务。
- 任务处理速度 > 任务提交速度 ,这样才能保证不会不断创建新的进程,避免内存被占满。
ScheduledThreadPool
在Java开发中,经常需要使用到定时任务跑一些数据。而常用的定时任务框架有quartz,著名的框架Spring已经实现了对quartz的支持,使用起来非常方便。在定时任务的节点为单一节点的时候,我们也可以使用Spring Task来完成定时任务,而Spring Task底层使用的就是ScheduledThreadPool
。如果定时任务是多节点的话,就需要使用分布式任务调度框架了,单纯的Spring Task是不行的,quartz是可以实现分布式任务调度的,quartz是使用数据库行锁来实现分布式的,另外在国内常用的分布式任务式框架还有当当开源的elastic-job,使用zookeeper来实现分布式。
回到ScheduledThreadPool
,在Java中,使用该线程池去执行任务,可以实现定时执行任务的效果。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
我们查看ScheduledThreadPoolExecutor
的构造方法,我们可以发现,该线程池的最大线程数可以看成是无限的,但是如果真的开启那么多个线程的话,内存应该要爆炸;另外,没有超时时间,最后他的WorkerQueue
是DelayedWorkQueue
,和其他几种线程池都不一样,这个并发阻塞队列,后面会分析。
总结
虽然线程池的源码并不是很难,但是不得不说,大神对线程池的设计还是超厉害的,使用不同的参数实现ThreadPoolExecutor
构造方法,就能达到不同的处理策略。这种设计能力确实是强无敌,只能仰望哇。