Java线程池ThreadPoolExecutor(二)

1 创建线程池

使用ThreadPoolExecutor构造器来创建线程池

现在来看看workQueue(任务队列)如何设置。

workQueue是BlockingQueue<Runnable>的子类,有以下可选项:

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个��入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

1.2 使用Executors工厂类

Executors类中提供了很多工厂方法,可以创建几种类型的ThreadPoolExecutor。

FixedThreadPool

固定线程数的线程池,corePoolSize和maximumPoolSize设置为相同的值,即全是核心线程,无法创建应急线程,线程池中线程也不会因空闲而终止。适用于任务量已知,相对耗时的任务。

 使用无界队列LinkedBlockingQueue作为工作队列(容量为Integer.MAX_VALUE),有如下影响:

  1. 当线程池中的线程数达到corePoolSize后,新任务都将放入无界队列中等待;
  2. maximumPoolSize、keepAliveTime将是无效参数。
  3. 运行中的FixedThreadPool不会拒绝任何任务,因此较难感知到任务是否积压。
SingleThreadExecutor

整理了这份Java面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

corePoolSize和maximumPoolSize被设置为1,其他参数与FixedThreadPool相同。 

CachedThreadPool

这是一个会根据需要动态创建新线程的线程池。

  • corePoolSize设置为0,maximumPoolSize被设置为Integer.MAX_VALUE,相当于可以不受限制的创建应急线程。
  • keepAliveTime设置为60L,空闲线程超过60秒后将会被终止。
  • 使用没有容量的SynchronousQueue作为线程池的工作队列。如果提交任务的速度高于已有线程处理速度时,CachedThreadPool会不断创建新线程。极端情况下,会因为创建过多线程而耗尽CPU和内存资源。

CachedThreadPool适合任务数比较密集,但每个任务执行时间较短的情况。 

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务。功能与Timer类似,但它的功能更强大、更灵活。 

 DelayQueue是一个无界队列,因此maximumPoolSize失去意义。

当调用scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。 ScheduledFutureTask主要包含3个成员变量:

  • long time,表示这个任务将要被执行的具体时间。
  • long sequenceNumber,表示这个任务被添加到ScheduledThreadPoolExecutor中的序号。
  • long period,表示任务执行的间隔周期。

 DelayQueue封装了一个PriorityQueue,会对队列中的ScheduledFutureTask进行排序,time小的排在前面,time相同时sequenceNumber小的排在前面(先提交的任务将被先执行)。 

当某个线程执行一个到期任务时,会将任务的time更新为下次执行时间,并重新放回队列中排序。 

2 关闭线程池

ThreadPoolExecutor有两个方法可以关闭线程池:shutdown和shutdownNow。原理都是遍历所有工作线程,逐个调用Thread.interrupt()来中断线程。

共同点:当调用以上任意一个方法后,ThreadPoolExecutor.isShutdown会返回true,且线程池将拒绝提交新的任务。 

 它俩的区别在于:

  • shutdown设置线程池状态为SHUTDOWN,只会interruptIdleWorkers,即立即中断等待任务的线程,而对正在执行任务的线程不会中断。即执行中的任务会继续执行,任务队列中剩余任务也会被执行。  
  • shutdownNow设置线程池状态为STOP,会调用interruptIfStarted,即中断所有已经启动的线程,即使线程正在某个执行,并返回任务队列中的剩余任务。 

该使用哪一个方法呢?这取决于任务特性,通常使用shutdown;如果任务不一定要执行完,则可以调用shutdownNow。

注意:如果任务无法响应,线程可能不会如期终止。如下面的代码:由于runnable1无法响应interrupt,导致线程池始终有活跃线程。

 

java

代码解读

复制代码

import lombok.extern.slf4j.Slf4j; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; @Slf4j(topic = "c.ShutdownThread") public class ShutdownThread { public static final ThreadPoolExecutor EXECUTOR_SERVICE = (ThreadPoolExecutor) Executors.newFixedThreadPool(3); public static void main(String[] args) throws InterruptedException { // 启动3个基本线程 EXECUTOR_SERVICE.prestartAllCoreThreads(); Runnable runnable1 = () -> { //用死循环模拟没有超时时间的 RPC调用 while (true) { } }; Runnable runnable2 = () -> { try { TimeUnit.SECONDS.sleep(15); } catch (InterruptedException e) { log.info("runnable2 被中断"); throw new RuntimeException(e); } }; EXECUTOR_SERVICE.submit(runnable1); EXECUTOR_SERVICE.submit(runnable2); TimeUnit.SECONDS.sleep(3); EXECUTOR_SERVICE.shutdownNow(); TimeUnit.SECONDS.sleep(3); log.info("活跃线程数" + EXECUTOR_SERVICE.getActiveCount()); if (EXECUTOR_SERVICE.isTerminated()) { log.info("线程池终止"); } } }

3 合理配置线程池

想要合理配置线程池,得先分析任务特性:

  • 任务性质:CPU密集型任务、IO密集型任务和混合型任务
  • 任务优先级:高、中和低。
  • 任务执行时间:长、中和短。
  • 任务依赖性:是否依赖其他系统资源,如数据库连接、网络连接等。 有以下建议:
  1. 性质不同的任务,可以分开使用不同的线程池。
  2. CPU密集型任务应配置尽可能小的线程,如配置CPU数+1个线程的线程池;
  3. IO密集型任务,线程并不是一直在占用CPU,应配置尽可能多的线程,如2倍cpu数;
  4. 优先级不同的任务,可以使用PriorityBlockingQueue,它可以让优先级高的任务先执行。
  5. 依赖于其他系统资源的任务,往往CPU空闲时间越长,可以设置更大的线程数。
  6. 建议使用有界队列,且拒绝策略能够给出提示或告警,这将增加线程池的稳定性和预警能力。

4 线程池监控

监控线程池,方便在出现问题时能够快速定位和处理。可调用ThreadPoolExecutor的这些方法:

  • getTaskCount:已提交任务的大致总数;
  • getCompletedTaskCount:线程池已完成的任务数量,是个近似值。
  • getLargestPoolSize:线程池曾经创建过的最大线程数量,判断线程池是否曾经满过。
  • getPoolSize:线程池的线程数量,即ThreadPoolExecutor.workers.size();
  • getActiveCount:获取活动的线程数。 此外,通过继承ThreadPoolExecutor来自定义线程池,重写beforeExecute、afterExecute和terminated方法,在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。 

AI 提问

标签:

Java

  • 12
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值