前言
一、线程池的使用场景
1. 加快请求响应(响应时间优先)
2. 加快处理大任务(吞吐量优先)
二、线程池的创建及重要参数
三、线程池中的线程创建流程
四、workQueue队列
五、常见的几种自动创建线程池方式
六、handler的拒绝策略
七、线程池的关闭方式
八、线程池实现线程复用的原理
九、ThreadPoolExecutor#execute 源码分析
十、手动创建线程池(推荐)
十一、Springboot中使用线程池
十二、beforeExecute和afterExecute
十三、补充
1.Callable和Runnable
2.Future和FutureTask
3.实现优先使用运行线程及调整线程数大小的线程池(线程池的优化)
4.coreSize满了优先创建线程
总结
前言
众所周知,创建线程的方式有以下几种:
1.继承Thread类并重写run()方法,然后调用start()方法启动线程。
2.实现Runnable接口,然后创建Thread对象并将Runnable对象作为参数传递给Thread的构造方法,最后调用start()方法启动线程。
3.实现Callable接口,然后创建FutureTask对象并将Callable对象作为参数传递给FutureTask的构造方法,最后创建Thread对象并将FutureTask对象作为参数传递给Thread的构造方法,最后调用start()方法启动线程。
4.使用线程池创建线程,Java提供了Executor和ExecutorService接口以及ThreadPoolExecutor类来创建线程池。可以通过调用Executor的静态工厂方法来创建线程池。例如,Executors.newFixedThreadPool()方法创建一个固定线程数的线程池。
5.使用ScheduledExecutorService接口创建定时执行任务的线程池,可以通过ScheduledExecutorService的静态工厂方法来创建线程池,例如,Executors.newScheduledThreadPool()方法创建一个定时执行任务的线程池。
其中,使用线程池创建线程是我们日常开发中最常使用的一种方式,因此本篇就着重来介绍并详解一下线程池的基本概念和常见的使用方法。
java中经常需要用到多线程来处理一些业务,我们非常不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源、线程上下文切换问题。同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理。java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。
比如用户在饿了么上查看某商家外卖,需要聚合商品库存、店家、价格、红包优惠等等信息返回给用户,接口逻辑涉及到聚合、级联等查询,从这个角度来看接口返回越快越好,那么就可以使用多线程方式,把聚合/级联查询等任务采用并行方式执行,从而缩短接口响应时间。这种场景下使用线程池的目的就是为了缩短响应时间,往往不去设置队列去缓冲并发的请求,而是会适当调高corePoolSize和maxPoolSize去尽可能的创造线程来执行任务。
比如业务中台每10分钟就调用接口统计每个系统/项目的PV/UV等指标然后写入多个sheet页中返回,这种情况下往往也会使用多线程方式来并行统计。和"时间优先"场景不同,这种场景的关注点不在于尽可能快的返回,而是关注利用有限的资源尽可能的在单位时间内处理更多的任务,即吞吐量优先。这种场景下我们往往会设置队列来缓冲并发任务,并且设置合理的corePoolSize和maxPoolSize参数,这个时候如果设置了太大的corePoolSize和maxPoolSize可能还会因为线程上下文频繁切换降低任务处理速度,从而导致吞吐量降低。
以上两种使用场景和JVM里的ParallelScavenge和CMS垃圾收集器有较大的类比性,ParallelScavenge垃圾收集器关注点在于达到可观的吞吐量,而CMS垃圾收集器重点关注尽可能缩短GC停顿时间。
线程池可以自动创建也可以手动创建,自动创建体现在Executors工具类中,常见的可以创建newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool;手动创建体现在可以灵活设置线程池的各个参数,体现在代码中即ThreadPoolExecutor类构造器上各个实参的不同:
public static ExecutorService newFixedThreadPool(int var0) {
return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue());
}
public static ExecutorService newSingleThreadExecutor() {
return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue());
}
public static ScheduledExecutorService newScheduledThreadPool(int var0) {
return new ScheduledThreadPoolExecutor(var0);
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {……}
ThreadPoolExecutor中重要的几个参数详解
举个例子:现有一个线程池,corePoolSize=10,maxPoolSize=20,队列长度为100,那么当任务过来会先创建10个核心线程数,接下来进来的任务会进入到队列中直到队列满了,会创建额外的线程来执行任务(最多20个线程),这个时候如果再来任务就会执行拒绝策略。
线程创建的流程
在任务不断增加的过程中,线程池会逐一进行以下 4 个方面的判断
核心线程数(corePoolSize)
任务队列(workQueue)
最大线程数(maximumPoolSize)
拒绝策略
自动创建线程池的几种方式都封装在Executors工具类中:
所以根据上面分析我们可以看到,FixedThreadPool和SigleThreadExecutor中之所以用LinkedBlockingQueue无界队列,是因为设置了corePoolSize=maxPoolSize,线程数无法动态扩展,于是就设置了无界阻塞队列来应对不可知的任务量;而CachedThreadPool则使用的是SynchronousQueue同步移交队列,为什么使用这个队列呢?因为CachedThreadPool设置了corePoolSize=0,maxPoolSize=Integer.MAX_VALUE,来一个任务就创建一个线程来执行任务,用不到队列来存储任务;SchduledThreadPool用的是延迟队列DelayedWorkQueue。在实际项目开发中也是推荐使用手动创建线程池的方式,而不用默认方式,关于这点在《阿里巴巴开发规范》中是这样描述的:
什么是线程复用?
在线程池中,通过同一个线程去执行不同的任务,这就是线程复用。
假设现在有 100 个任务,我们创建一个固定线程的线程池(FixedThreadPool),核心线程数和最大线程数都是 3,那么当这个 100 个任务执行完,都只会使用三个线程。
示例:
public class FixedThreadPoolDemo {
static ExecutorService executorService = Executors.newFixedThreadPool(3);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + "->
执行");
});
}
// 关闭线程池
executorService.shutdown();
}
}
执行结果:
pool-1-thread-1-> 执行
pool-1-thread-2-> 执行
pool-1-thread-3-> 执行
pool-1-thread-1-> 执行
pool-1-thread-3-> 执行
pool-1-thread-2-> 执行
pool-1-thread-3-> 执行
pool-1-thread-1-> 执行
线程复用的原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来
java.util.concurrent.ThreadPoolExecutor#execute
public void execute(Runnable command) {
// 如果传入的Runnable的空,就抛出异常
if (command == null)
throw new NullPointerException();
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);
}
//如果传入的Runnable的空,就抛出异常 if (command == null) throw new NullPointerException();
execute 方法中通过 if 语句判断 command ,也就是 Runnable 任务是否等于 null,如果为 null 就抛出异常。
if (workerCountOf© < corePoolSize) { if (addWorker(command, true)) return; c = ctl.get(); }
判断当前线程数是否小于核心线程数,如果小于核心线程数就调用 addWorker() 方法增加一个 Worker,这里的 Worker 就可以理解为一个线程。
addWorker 方法的主要作用是在线程池中创建一个线程并执行传入的任务,如果返回 true 代表添加成功,如果返回 false 代表添加失败。
第一个参数表示传入的任务
第二个参数是个布尔值,如果布尔值传入 true 代表增加线程时判断当前线程是否少于 corePoolSize,小于则增加新线程(核心线程),大于等于则不增加;同理,如果传入 false 代表增加线程时判断当前线程是否少于 maximumPoolSize,小于则增加新线程(非核心线程),大于等于则不增加,所以这里的布尔值的含义是以核心线程数为界限还是以最大线程数为界限进行是否新增非核心线程的判断
这一段判断相关源码如下
private boolean addWorker(Runnable firstTask, boolean core) {
...
int wc = workerCountOf(c);//当前工作线程数
//判断当前工作线程数>=最大线程数 或者 >=核心线程数(当
core = true)
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
...
最核心的就是 core ? corePoolSize : maximumPoolSize 这个三目运算。
// 核心线程已满,但是任务队列未满,添加到队列中
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);
}
如果代码执行到这里,说明当前线程数大于或等于核心线程数或者 addWorker 失败了,那么就需要通过
if (isRunning© && workQueue.offer(command)) 检查线程池状态是否为 Running,如果线程池状态是 Running 就通过 workQueue.offer(command) 将任务放入任务队列中,
任务成功添加到队列以后,再次检查线程池状态,如果线程池不处于 Running 状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略,代码如下:
线程复用源码分析
java.util.concurrent.ThreadPoolExecutor#runWorker
省略掉部分和复用无关的代码之后,代码如下
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // 释放锁 设置work的state=0 允许中断
boolean completedAbruptly = true;
try {
//一直执行 如果task不为空 或者 从队列中获取的task不为空
while (task != null || (task = getTask()) != null) {
task.run();//执行task中的run方法
}
}
completedAbruptly = false;
} finally {
//1.将 worker 从数组 workers 里删除掉
//2.根据布尔值 allowCoreThreadTimeOut 来决定是否补充新的
Worker 进数组 workers
processWorkerExit(w, completedAbruptly);
}
}
可以看到,实现线程复用的逻辑主要在一个不停循环的 while 循环体中。
通过获取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务
直接通过 task.run() 来执行具体的任务(而不是新建线程)
在这里,我们找到了线程复用最终的实现,通过取 Worker 的 firstTask 或者 getTask 方法从 workQueue 中取出了新任务,并直接调用 Runnable 的 run 方法来执行任务,也就是如之前所说的,每个线程都始终在一个大循环中,反复获取任务,然后执行任务,从而实现了线程的复用。
那么上面说了使用Executors工具类创建的线程池有隐患,那如何使用才能避免这个隐患呢?对症下药,建立自己的线程工厂类,灵活设置关键参数:
//这里默认拒绝策略为AbortPolicy
private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));
使用guava包中的ThreadFactoryBuilder工厂类来构造线程池:
private static ThreadFactory threadFactory = new ThreadFactoryBuilder().build();
private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(10), threadFactory, new ThreadPoolExecutor.AbortPolicy());
通过guava的ThreadFactory工厂类还可以指定线程组名称,这对于后期定位错误时也是很有帮助的
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-d%").build();
springboot可以说是非常流行了,下面说说如何在springboot中优雅的使用线程池
/**
* @ClassName ThreadPoolConfig
* @Description 配置类中构建线程池实例,方便调用
* @Author simonsfan
* @Date 2018/12/20
* Version 1.0
*/
@Configuration
public class ThreadPoolConfig {
@Bean(value = "threadPoolInstance")
public ExecutorService createThreadPoolInstance() {
//通过guava类库的ThreadFactoryBuilder来实现线程工厂类并设置线程
名称
ThreadFactory threadFactory = new
ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build();
ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 60L,
TimeUnit.SECONDS, new ArrayBlockingQueue(100),
threadFactory, new ThreadPoolExecutor.AbortPolicy());
return threadPool;
}
//通过name=threadPoolInstance引用线程池实例
@Resource(name = "threadPoolInstance")
private ExecutorService executorService;
@Override
public void spikeConsumer() {
//TODO
executorService.execute(new Runnable() {
@Override
public void run() {
//TODO
}});
}
在ThreadPoolExecutor类中有两个比较重要的方法引起了我们的注意:beforeExecute和afterExecute
protected void beforeExecute(Thread var1, Runnable var2) {
}
protected void afterExecute(Runnable var1, Throwable var2) {
}
这两个方法是protected修饰的,很显然是留给开发人员去重写方法体实现自己的业务逻辑,非常适合做钩子函数,在任务run方法的前后增加业务逻辑,比如添加日志、统计等。这个和我们springmvc中拦截器的preHandle和afterCompletion方法很类似,都是对方法进行环绕,类似于spring的AOP,参考下图:
Runnable和Callable都可以理解为任务,里面封装这任务的具体逻辑,用于提交给线程池执行,区别在于Runnable任务执行没有返回值,且Runnable任务逻辑中不能通过throws抛出cheched异常(但是可以try catch),而Callable可以获取到任务的执行结果返回值且抛出checked异常。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
@FunctionalInterface
public interface Callable {
V call() throws Exception;
}
Future接口用来表示执行异步任务的结果存储器,当一个任务的执行时间过长就可以采用这种方式:把任务提交给子线程去处理,主线程不用同步等待,当向线程池提交了一个Callable或Runnable任务时就会返回Future,用Future可以获取任务执行的返回结果。Future的主要方法包括:
下面来实际演示Future和FutureTask的用法:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future future = executorService.submit(new Task());
Integer integer = future.get();
System.out.println(integer);
executorService.shutdown();
}
static class Task implements Callable {
@Override
public Integer call() throws Exception {
System.out.println("子线程开始计算");
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
当前在JDK中默认使用的线程池 ThreadPoolExecutor,在具体使用场景中,有以下几个缺点
core线程一般不会timeOut
新任务提交时,如果工作线程数小于 coreSize,会自动先创建线程,即使当前工作线程已经空闲,这样会造成空闲线程浪费
设置的maxSize参数只有在队列满之后,才会生效,而默认情况下容器队列会很大(比如1000)
如一个coreSize为10,maxSize为100,队列长度为1000的线程池,在运行一段时间之后的效果会是以下2个效果:
空闲线程优先
空闲线程优先在基本逻辑中,即如果线程数小于coreSize,但如果有空闲线程,就取消创建线程的逻辑. 在有空闲线程的情况下,直接将任务放入队列中,即达到任务执行的目的。
这里的逻辑即是直接调整默认的ThreadPoolExecutor逻辑,通过重载 execute(Runnable) 方法达到效果. 具体代码如下所示:
public void execute(Runnable command) {
//此处优先处理有活跃线程的情况,避免在
从之前的逻辑来看,如果放入队列失败,则尝试创建新线程。在这个时候,相应的coreSize肯定已经满了。那么,只需要处理一下逻辑,将其offer调整为false,即可以实现相应的目的。
这里的逻辑,即是重新定义一个BlockingDeque,重载相应的offer方法,相应的参考如下:
public boolean offer(Runnable o) {
//这里的parent为ThreadPoolExecutor的引用
int poolSize = parent.getPoolSize();
int maxPoolSize = parent.getMaximumPoolSize();
//还没到最大值,先创建线程
if(poolSize < maxPoolSize) {
return false;
}
//默认逻辑
return super.offer(o);
}
即判定当前线程池中线程数如果小于最大线程数,即直接返回false,达到放入队列失败的效果。
总结
按照以上的调整,只需要通过继承自默认的ThreadPoolExecutor和默认的BlockingQueue(如LinkedBlockingDeque),重载2个主要的方法 ThreadPoolExecutor#execute 和 LinkedBlockingDeque#offer 即达到调整的目的。
5.线程池中的异常状况
当线程池中的一个线程出现异常时,线程池会根据预先设置的策略进行处理。具体处理方式取决于线程池的类型和配置,以下是常见的处理方式:
FixedThreadPool:线程池中的线程出现异常后会立刻终止,同时线程池会创建一个新的线程来替换该线程并继续执行任务。
CachedThreadPool:线程池中的线程出现异常后会被标识为无效线程,线程池会创建一个新的线程来替换该线程并继续执行任务。
ScheduledThreadPool:与FixedThreadPool类似,线程池中的线程出现异常后会立刻终止,同时线程池会创建一个新的线程来替换该线程并继续执行任务。
无论哪种类型的线程池,如果线程出现异常,都应该尽快查找问题所在并进行修复,以保证系统的稳定性和可靠性。可以使用try-catch块来捕获异常并打印日志或进行其他处理。同时,可以使用线程池的execute(Runnable command)方法来提交任务,这样线程池会为每个任务创建一个新的线程,从而减少单个线程出现异常对系统的影响。
本文从线程的创建方式为引入点,详细论述了线程池的基本概念、部分源码及常见的使用方法等。基本囊括了工作中用到的线程池的各种情况及使用方法,可供初级开发者用作线程池的学习资料,中高级开发者的问题排查手册。