Java线程池ThreadPoolExecutor简介(一)

本文转自:https://blog.csdn.net/guozebo/article/details/51057516

在多任务并发的应用场景,线程池ThreadPoolExecutor是必不可少的。使用线程池最主要的好处就是能够限制系统最大线程并发数、空余线程复用、线程统一管理、维护一些统计数据如活跃线程数等等。但我感触最深的是它特别适用于生产者_消费者场景,感觉就是为这种模式而设计的工具。生产者负责创建任务对象(可以是Thread、Runnable、Callable的子类),然后提交到线程池(严格来说是线程池中的队列),而消费者自然是线程池中的线程。还有一定要get的技能是线程池中的线程数量是可以动态调整的!


构造ThreadPoolExecutor参数介绍

ThreadPoolExecutor提供了几个构造方法,个别参数不传的话就使用默认值,这里我们介绍包含所有参数的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数简介:
  1. corePoolSize 核心线程数,即线程池中保留的线程数
  2. maximumPoolSize 线程池中最大线程数,超过若线程数超过该值将触发拒绝策略RejectedExecutionHandler 
  3. keepAliveTime 当一个线程处于空闲状态是,超过keepAliveTime 长时间将终止该线程,以便池中线程数量恢复到corePoolSize大小,若线程池中数量小于corePoolSize 则该设置不起作用。
  4. unit 是一个时间枚举,表示 keepAliveTime 的单位,如 TimeUnit.SECONDS
  5. workQueue 表示存放任务的队列,比较常用是LinkedBlockingQueue、SynchronousQueue
  6. threadFactory 产生线程的工厂类,可参考默认实现Executors.DefaultThreadFactory。由于经常要在日志中打印出线程名,因此可以参考该实现来实现自定义线程名,或是使用第三方工具类。
  7. handler 拒绝策略

提交任务流程

线程池虽然提供了不同参数的方法来供外部提交任务,但最终调用的都是 ThreadPoolExecutor.execute(Runnable command) (强烈推荐查看该方法源码)。以下是任务提交流程:


提交任务时,根据当前线程池中的线程数量currentNum不同,流程如下:
  1. 若currentNum小于corePoolSize,则不管有没有空闲线程,都创建新的线程来执行该任务
  2. 若currentNum等于corePoolSize,但缓冲队列 workQueue未满,那么任务被放入缓冲队列,等待调度执行
  3. 若currentNum大于corePoolSize,且缓冲队列 workQueue已满,并且currentNum小于maximumPoolSize,继续创建新的线程来执行该任务
  4. 若currentNum大于corePoolSize,且缓冲队列 workQueue已满,并且currentNum等于maximumPoolSize,新提交任务由拒绝策略Handler处理
当然还是建议看看源码:

public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
/**
* step 1:
* 通过workerCountOf(c)返回池中已存在worker数量,即线程数
* 若小于corePoolSize则新增worker来执行该任务
*/
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
/**
* step 2:
* 若以上条件不成立,则尝试放进Queue中
* 注意Queue使用的是offer(),若队列已满返回false,不会阻塞线程
*/
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);
}
/**
* step 3:
* 若以上条件不成立,则尝试新增worker来执行该任务
* 若不成功(workerCountOf(c) >= maximumPoolSize)
* 直接触发 拒绝策略
*/
else if (!addWorker(command, false))
reject(command);
}

队列类型

虽然线程池要求队列必须是BlockingQueue的子类,但从上面的源码中可以到添加任务使用的是非阻塞的offer方法,该方法只会在队列满的时候直接返回false。因此不要期望当队列已满的时候,添加任务的线程会被阻塞!
  1. 无队列  通常使用 SynchronousQueue。通过JDK文档可以了解到这是阻塞队列,且队列长度为0,意味着一个线程往该队列添加元素时将会被阻塞住,直到有另外一个线程从队列中取出该元素,从效果上看“添加”和“取出”这两个操作“同步”了。但上面的源码中看到添加任务使用的是非阻塞的offer方法,对这个队列而言,若没有线程在等待领取任务,则offer返回false。如果你希望添加的任务不需要“排队”,直接执行,就可以使用该队列,但要求将maximumPoolSize设置得很大以便不会触发拒绝策略
  2. 有界队列  通常使用LinkedBlockingQueue ,根据实际使用场景定义一个固定长的队列,需要综合考虑任务并发量,线程数等等
  3. 无界队列  容量很大的有界队列,如直接new LinkedBlockingQueue()返回一个长度为 Integer.MAX_VALUE 的队列

拒绝策略

ThreadPoolExecutor内部为RejectedExecutionHandler 接口实现了4种拒绝策略,强烈推荐看其源码实现!
  1. ThreadPoolExecutor.AbortPolicy 抛出抛出java.util.concurrent.RejectedExecutionException异常,默认值
  2. ThreadPoolExecutor.CallerRunsPolicy 交由调用者线程来执行任务
  3. ThreadPoolExecutor.DiscardPolicy 什么都不做,相当于抛弃该任务
  4. ThreadPoolExecutor.DiscardOldestPolicy 位于任务队列头部的任务将被删除,以便队列能加入该任务

ThreadFactory介绍

线程池的构造方法中允许设置ThreadFactory的接口实现,最常用的场景就是工厂类中自定义线程的名字,要知道线程名在大多数情况下都应该加到日志参数中。虽然默认实现Executors.DefaultThreadFactory不支持定义线程名,但可以通过查看其源码来实现。若是有用到第三方类库如guava,它也提供了ThreadFactory的实现,如下:
Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("doMyTask-%d").build());
通过查看guava的ThreadFactoryBuilder源码得知它使用了代理模式,我也模仿了一下它的实现:
public class MyThreadFactory implements ThreadFactory{
private final ThreadFactory backingThreadFactory = Executors.defaultThreadFactory();
private final AtomicLong count = new AtomicLong(0);
private final String nameFormat;
public MyThreadFactory(String nameFormat) {
super();
this.nameFormat = nameFormat;
}
@Override
public Thread newThread(Runnable runnable) {
Thread thread = backingThreadFactory.newThread(runnable);
thread.setName(String.format(nameFormat, count.getAndIncrement()));
return thread;
}
}

线程池构造工具类

由于线程池的参数设置比较复杂,因此JDK推荐使用工具类java.util.concurrent.Executors来创建几种常用的线程池,我们也可以通过阅读源码来进一步加深参数的理解。
(1)创建一个固定线程数的线程池。注意这里的队列相当于一个无界队列,其长度为 Integer.MAX_VALUE
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
(2)创建一个线程数无上限的线程池,其线程数可以不断增加。因此这里使用了队列类型为SynchronousQueue,即无队列模式。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
一定要记得在实际应用中,我们不可能将maximumPoolSize设置成Integer.MAX_VALUE,或传入长度为 Integer.MAX_VALUE的队列new LinkedBlockingQueue<Runnable>(),要是程序有bug就会导致线程数过大而压垮了线上系统,或内存溢出。对待线上系统一定要十分谨慎,保守的设置这些参数!

几个API说明

/**
* 设置线程池核心线程数,该值将覆盖构造器中的值(实现动态设置并发的关键配置)
* 若新值比原有的值小,那意味着有部分线程在处于空闲状态后会被终止
* 若新值比原有的值大,且队列有等待任务,就会马上创建新的线程来处理
*/
public void setCorePoolSize(int corePoolSize);
/**
* 返回当前正在执行任务的线程数,只是个大约值!!最好不要作为判断当前线程数大小,可以作为监控信息
* 其底层通过遍历worker实例的标志作为判断,但是会漏掉那些领到任务但还没刷新标志的worker
*/
public int getActiveCount();
/**
* 关闭线程池
* 不允许添加新的任务,但会等待池中所有的任务执行完毕
*/
public void shutdown();
/**
* 关闭线程池
* 不允许添加新的任务,停止任务调度,并返回那些尚未执行的任务
* 中断那些正在执行的线程,(看源码)其实就是调用worker线程的interrupt()方法
* 注意要是那些线程没有实现响应中断的逻辑,则停止不了线程
*/
public List<Runnable> shutdownNow();
/**
* 在调用上面两个方法进行关闭线程池后
* 调用这个方法将使当前线程阻塞住,直到池中所有任务执行完成或超时
*/
public boolean awaitTermination(long timeout, TimeUnit unit);
其实JDK的注释都写得详细了,另外除非真的对这个API很熟悉,曾经有用过,不然我都会写demo来测试一下。

线程池的状态变量说明

如果想看懂源码,ThreadPoolExecutor有个成员变量ctl 需要特别说明一下,源码如下:

源码的文档中已经标注了这些状态之间的转换关系:
RUNNING -> SHUTDOWN On invocation of shutdown(), perhaps implicitly in finalize()
(RUNNING or SHUTDOWN) -> STOP On invocation of shutdownNow()
SHUTDOWN -> TIDYING When both queue and pool are empty
STOP -> TIDYING When pool is empty
TIDYING -> TERMINATED When the terminated() hook method has completed

动态调整线程池的并发数

线程池的setCorePoolSize()方法经常用于动态调整线程池的并发数,并且我之前也认为该设置是马上生效的,即线程数会马上增加或减少到新设置的值。但通过写demo测试后发现如果设置的值比原值大,线程数确实是马上增加了,但如果新设置的值比原来的小,却没有马上生效,到底是为什么呢?查看了一下setCorePoolSize的源码,如下:
public void setCorePoolSize(int corePoolSize) {
if (corePoolSize < 0)
throw new IllegalArgumentException();
int delta = corePoolSize - this.corePoolSize;
/**
* 覆盖构造方法中设置的值
*/
this.corePoolSize = corePoolSize;
if (workerCountOf(ctl.get()) > corePoolSize)
/**
* 如果新值比原值小,则中断那些处理空闲的worker
*/
interruptIdleWorkers();
else if (delta > 0) {
/**
* 如果新值比原值大,且队列中还存在任务,则马上新增worker来处理
*/
int k = Math.min(delta, workQueue.size());
while (k-- > 0 && addWorker(null, true)) {
if (workQueue.isEmpty())
break;
}
}
}
可以看到,如果新设置的值比原来的小,此时会终止掉那些处于 空闲的worker。如果当时所有worker都在处理任务,不存在空闲的worker,那调用setCorePoolSize()并不会产生直接的效果。

因此接下来要了解一下线池中的工作线程worker的生命周期,了解worker线程什么时候会结束,看看corePoolSize和maximumPoolSize等参数是如何起作用的。
我们都知道worker是一个线程,启动之后就会不断轮询任务队列,若能取到任务就处理,大致流程如下(参考 ThreadPoolExecutor.getTask() 方法):

这个流程能够得到的结论和之前是一致的,如下:
(1)如果当前线程数小于corePoolSize,那该线程一定不会被停止
(2)若当前线程数大于corePoolSize且小于maximumPoolSize ,那就要看在keepAliveTime时间内能不能取到任务(原来keepAliveTime是这样起作用的)
(3)若当前线程数大于maximumPoolSize ,那该线程就会马上被停止掉

综上所述:如果想马上减少线程池的并发数,最好就是同时减小 corePoolSize 和 maximumPoolSize  !!

上面好多的结论都是通过写demo来验证得出的,比如为了验证setCorePoolSize()方法,demo代码如下:
public class SetCoreSizeDemo implements Runnable{
private ThreadPoolExecutor threadPool;
private final int queueSize = 10;
private final int maximumPoolSize = 10;
private int corePoolSize = 5;
private long keepAliveTime = 10;
public final static AtomicInteger runCount = new AtomicInteger(0);
//test
public static void main(String[] args) {
new SetCoreSizeDemo();
}
public SetCoreSizeDemo() {
threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
keepAliveTime, TimeUnit.SECONDS ,new LinkedBlockingQueue<Runnable>(queueSize));
//监控线程池中待处理任务数量,和活跃线程数
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("queueSize: "+threadPool.getQueue().size()
+" ,activityCount: "+threadPool.getActiveCount());
}
}, 100L, 500L, TimeUnit.MILLISECONDS);
//模拟在20秒后减小并发数(当然你可以增加),继续观察活跃线程数
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("\n\nchanging coreSize: "+2);
threadPool.setCorePoolSize(2);
}
}, 20L, 200000L, TimeUnit.SECONDS);
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
this, 5L, 1L, TimeUnit.SECONDS);
}
@Override
public void run() {
try {
// 为了模拟一段时间内没有向线程池添加任务,让线程处于空闲状态
// 注意这段时间要大于线程池的keepAliveTime,才会让活跃线程数降到corePoolSize大小
int count = runCount.incrementAndGet();
if(count > 50 && count < 100) {
return;
}
while( threadPool.getQueue().size() < queueSize ) {
threadPool.execute(new MyTask());
}
} catch(Exception e) {
e.printStackTrace();
}
}
static class MyTask implements Runnable{
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

参考


猜你喜欢

转载自blog.csdn.net/qq_40074764/article/details/81048627