前言
本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和参考文献请见100个问题搞定Java并发
正文
构造函数
先来看一下 ThreadPoolExecutor 类最重要的构造函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
函数的参数含义如下。
- corePoolSize :指定了线程池中的线程数量。
- maximumPoolSize :指定了线程池中的最大线程数量。
- keepAliveTime :当线程池线程数量超过 corePoolSize 时,多余的空闲线程的存活时间,即超过 corePoolSize 的空闲线程,在多长时间内会被销毁。
- unit : keepAliveTime 的单位。
- workQueue :任务队列,被提交但尚未被执行的任务。
- threadFactory :线程工厂,用于创建线程,一般用默认的即可。
- handler :拒绝策略。 当任务太多来不及处理时,如何拒绝任务。
以上参数中大部分都很简单,只有参数 workQueue 和 handler 需要进行详细说明。
workQueue
参数 workQueue 指被提交但未执行的任务队列,它是一个 BlockingQueue 接口的对象仅用于存放 Runnable 对象。
根据队列功能分类,在 ThreadPoolExecutor 类的构造函数中可使用以下几种 Blockingqueue 接口。
直接提交的队列
该功能由 Synchronousqueue 对象提供。
Synchronousqueue 是一个特殊的 Blockingqueue 。
Synchronousqueue 没有容量,每一个插入操作都要等待个相应的删除操作,反之,每一个删除操作都要等待对应的插入操作。
如果使用 Synchronousqueue ,则提交的任务不会被真实地保存,而总是将新任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程,如果线程数量已经达到最大值则执行拒绝策略。
因此,使用 Synchronousqueue 队列,通常要设置很大的 maximumPoolSize 值,否则很容易执行拒绝策略。
有界的任务队列
有界的任务队列可以使用 ArrayBlockingQueue 类实现
ArrayBlockingQueue 类的构造函数必须带一个容量参数,表示该队列的最大容量
public ArrayBlockingQueue ( int capacity )
当使用有界的任务队列时,若有新的任务需要执行,如果线程池的实际线程数小于 corePoolSize ,则会优先创建新的线程,若大于 corePoolSize ,则会将新任务加入等待队列。
若等待队列己满,无法加入,则在总线程数不大于 maximumPoolSize 的前提下,创建新的进程执行任务。
若大于 maximumPoolSize ,则执行拒绝策略。
可见,有界队列仅当在任务队列装满时,才可能将线程数提升到 corePoolSize 以上,换言之,除非系统非常繁忙,否则要确保核心线程数维持在 corePoolSize 。
无界的任务队列
无界任务队列可以通过 LinkedBlockingqueue 类实现。
与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。
当有新的任务到来,系统的线程数小于 corePoolSize 时,线程池会生成新的线程执行任务,但当系统的线程数达到 corePoolSize 后,就不会继续增加了。
若后续仍有新的任务加入,而又没有空闲的线程资源,则任务直接进入队列等待。
若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存优先任务队列:
优先任务队列是带有执行优先级的队列。
它通过 PriorityBlockingqueue 类实现,可以控制任务的执行先后顺序。
它是一个特殊的无界队列。
无论是有界队列 ArrayBlockingQueue 类,还是未指定大小的无界队列 LinkedQueue 类都是按照先进先出算法处理任务的。
而 PriorityBlockingqueue 类则可以根据任务自身的优先级顺序先后执行,在确保系统性能的同时,也能有很好的质量保证(总是确保高优先级的任务先执行)。
Executors 源码实现
请参考我的这篇博客——Executors 源码解析(JDK8)
newFixedThreadPool()
回顾 newFixedThreadPool() 方法的实现,它返回了一个 corePoolSize 和 maximumPoolSize 大小一样的,并且使用了 LinkedBlockingqueue 任务队列的线程池。
因为对于固定大小的线程池而言,不存在线程数量的动态变化,因此 corePoolSize 和 maximumPoolSize 可以相等。
同时,它使用无界队列存放无法立即执行的任务,当任务提交非常频繁的时候,该队列可能迅速膨胀,从而耗尽系统资源。
newSingleThreadExecutor()
newSingleThreadExecutor() 方法返回的单线程线程池,是 newFixedThreadPool() 方法的一种退化,只是简单地将线程池线程数量设置为 1 。
newCachedThreadPool()
newCachedThreadPool() 方法返回 corePoolSize 为 0 , maximumPoolSize 无穷大的线程池,这意味着在没有任务时,该线程池内无线程,而当任务被提交时,该线程池会使用空闲的线程执行任务,若无空闲线程,则将任务加入 SynchronousQueue 队列,而 SynchronousQueue 队列是一种直接提交的队列,它总会迫使线程池增加新的线程执行任务。
当任务执行完毕后,由于 corePoolsize 为 0 ,因此空闲线程又会在指定时间内( 60 秒)被回收。
对于 newCachedThreadPool() 方法,如果同时有大量任务被提交,而任务的执行又不那么快时,那么系统便会开启等量的线程处理,这样做可能会很快耗尽系统的资源。
注意:使用自定义线程池时,要根据应用的具体情况选择合适的并发队列作为任务的缓冲。 当线程资源紧张时,不同的并发队列对系统行为和性能的影响均不同。
拒绝策略
ThreadPoolExecutor 类的最后一个参数指定了拒绝策略。
也就是当任务数量超过系统实际承载能力时,就要用到拒绝策略了。
拒绝策略可以说是系统超负荷运行时的补救措施,通常由于压力太大而引起的,也就是线程池中的线程已经用完了,无法继续为新任务服务。
同时,等待队列中也已经排满了,再也放不下新任务了。
这时,我们就需要有一套机制合理地处理这个问题。
JDK 内置的拒绝策略如下。
- AbortPolicy 策略:该策略会直接抛出异常,阻止系统正常工作。
- CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。 显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
- DiscardoOldestPolicy 策略:该策略将丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
- DiscardPolicy 策略:该策略默默地丢弃无法处理的任务,不予任何处理。 如果允许任务丢失,我觉得这可能是最好的一种方案了吧!
以上内置的策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际应用的需要,完全可以自己扩展 RejectedExecutionHandler 接口。
这里是一个例子:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class RejectThreadPoolDemo {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread ID:"
+ Thread.currentThread().getId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
ExecutorService es = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new RejectedExecutionHandler(){
@Override
public void rejectedExecution(Runnable r,
ThreadPoolExecutor executor) {
System.out.println(r.toString()+" is discard");
}
});
for (int i = 0; i < Integer.MAX_VALUE; i++) {
es.submit(task);
Thread.sleep(10);
}
}
}
ThreadFactory
看了那么多有关线程池的介绍,不知道大家有没有思考过一个基本的问题:
线程池中的线程是从哪里来的呢?
线程池的主要作用是为了线程复用,也就是避免了线程的频繁创建。
但是,最开始的那些线程从何而来呢?答案就是 ThreadFactory 。
ThreadFactory 是一个接口,它只有一个用来创建线程的方法。
Thread newThread (Runnable r) ;
当线程池需要新建线程时,就会调用这个方法。
自定义线程池可以帮助我们做不少事。
比如,我们可以跟踪线程池究竟在何时创建了多少线程,也可以自定义线程的名称、组以及优先级等信息,甚至可以任性地将所有的线程设置为守护线程。
总之,使用自定义线程池可以让我们更加自由地设置线程池中所有线程的状态。
下面的案例使用自定义的 ThreadFactory ,一方面记录了线程的创建,另一方面将所有的线程都设置为守护线程,这样,当主线程退出后,将会强制销毀线程池。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TFThreadPoolDemo {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread ID:"
+ Thread.currentThread().getId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
ExecutorService es = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactory(){
@Override
public Thread newThread(Runnable r) {
Thread t= new Thread(r);
t.setDaemon(true);
System.out.println("create "+t);
return t;
}
}
);
for (int i = 0; i < 5; i++) {
es.submit(task);
}
Thread.sleep(2000);
}
}
ThreadPoolExecutor 核心源码(JDK8)
/**
* 在将来的某个时候执行给定的任务。
* 任务可以在新线程或现有池线程中执行。
* 如果由于此执行器已关闭或已达到其容量而无法提交任务执行,则该任务将由当前
* RejectedExecutionHandler处理。
*/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* 分三步进行:
* 1.如果正在运行的线程少于corePoolSize,请尝试以给定命令作为第一个线程启动新线程任务对
* addWorker的调用以原子方式检查运行状态和workerCount,从而防止可能增加在不应该的情
* 况下,通过返回false执行线程。
* 2.如果任务可以成功排队,那么我们仍然需要再次检查是否应该添加线程(因为自上次检查以来,有的
* 存活线程可能已经挂了)或自进入此方法后,线程池已关闭。所以我们重新检查状态,如有必要,在
* 以下情况下回滚到排队阶段:已停止,或者如果没有线程启动新线程。
* 3.如果我们无法将任务排队,那么我们将尝试添加一个新任务,如果失败了,我们知道我们已经被关闭
* 或饱和了所以拒绝这个任务。
*/
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);
}
代码中的 workerCountOf 函数取得了当前线程池的线程总数。
当线程总数小于 corePoolSize 核心线程数时,会将任务通过 addWorker 方法直接调度执行。
否则,则 workQueue.offer 进入等待队列。
如果进入等待队列失败 (比如有界队列到达了上限,或者使用了 SynchronousQueue 类) ,则会将任务直接提交给线程池。
如果当前线程数已经达到 maximumpoolsize ,则提交失败,就执行拒绝策略。