【并发编程-3】线程池

对于多线程来说,new Thread一定是创建了线程,而Runnable只是一个任务,并没有创建新的线程。 所以,Runnable任务要交给线程来执行。 如果对于每个任务都创建一个线程来执行,显然是不合理的。
线程池就是为了复用线程来处理多个任务,像tomcat, dubbo,数据库连接池等常用的框架都会使用线程池。 在自己开发的过程中,也可能会使用多线程的情况。 比如,一个请求的步骤太多,可以使用多线程并行执行。

线程池继承关系:

看两个核心类:ThreadPoolExecutor 和 ScheduledThreadPoolExecutor

ThreadPoolExecutor:

基本介绍:

java 复制代码
//...

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 存放任务的阻塞队列
private final BlockingQueue<Runnable> workQueue;
// 对线程池内部各种变量进行互斥访问控制
private final ReentrantLock mainLock = new ReentrantLock();
// 线程集合
private final HashSet<Worker> workers = new HashSet<Worker>();

线程池中的每一个线程就是一个 Worker 对象:

java 复制代码
private final class Worker extends AbstractQueuedSynchronizer implements
Runnable {
// ...
final Thread thread; // Worker封装的线程
Runnable firstTask; // Worker接收到的第1个任务
volatile long completedTasks; // Worker执行完毕的任务个数
// ...
}

由定义会发现, Worker 继承于 AQS ,也就是说 Worker本身就是一把锁,锁的就是worker对象本身。 lock 方法一旦获取了独占锁,表示当前线程正在执行任务中,那么它会有以下几个作用:

  1. 如果正在执行任务,则不应该中断线程;
  2. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
  3. 线程池在执行 shutdown 方法或 tryTerminate 方法时,会调用 interruptIdleWorkers 方法来中断空闲的线程,interruptIdleWorkers 中会使用 tryLock 方法来判断线程池中的线程是否是空闲状态。 说简单点,就是不能直接中断在任务中的线程,做到优雅的关闭。

核心参数:

java 复制代码
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
  1. corePoolSize:在线程池中始终维护的线程个数。
  2. maxPoolSize:在corePooSize已满、队列也满的情况下,扩充线程至此值。
  3. keepAliveTime/TimeUnit:maxPoolSize 中的空闲线程存活时间,销毁后线程数缩回corePoolSize。
  4. blockingQueue:线程池所用的队列类型。
  5. threadFactory:线程创建工厂,可以自定义,有默认值:Executors.defaultThreadFactory()
  1. RejectedExecutionHandler:corePoolSize已满,队列已满,maxPoolSize 已满,最后的拒绝策略。
    优雅关闭线程池:
    当关闭一个线程池的时候,有的线程还正在执行某个任务,有的调用者正在向线程池提交任务,并且队列中可能还有未执行的任务。因此,关闭过程需要一个平滑的过渡,这就涉及线程池的完整生命周期管理。
    线程池状态:

线程池五种状态: RUNNING 、 SHUTDOWN 、 STOP 、 TIDYING 和 TERMINATED 。 关闭时,状态值只能从小到大迁移:

在调用 shutdown() 或者 shutdownNow() 之后,线程池并不会立即关闭,接下来需要调用 awaitTermination() 来等待线程池关闭。
awaitTermination中就是不断循环,每隔500ms去校验一下线程池状态是否为TERMINATED,如果是,则返回。 该状态就是上面说的,worker释放锁后就会更新状态。

另外,shutdown()只会中断空闲的线程,shutdownNow()会中断所有线程,包括清空任务队列。

线程池执行过程:

  1. 执行execute方法向池中提交任务,如果核心线程数小于corePoolSize,即使池中还有其他空闲线程,也会创建新线程执行任务。
  2. 当达到corePoolSize,会放到队列中,空闲线程会从队列中取任务执行,在worker中其实就不断循环的过程。
  3. 队列已满,则会创建新线程,直到数量达到maxPoolSize,则会执行拒绝策略。
  4. 空闲时间超过keepAliveTime的线程,会被销毁,直到数量收缩到corePoolSize。 当然,如果线程池设置了allowCoreThreadTimeOut,只要空闲时间超过的线程都会被销毁。

线程池拒绝策略:

在线程池的execute方法的最后,也就是不满足执行条件时,就会执行拒绝策略。 创建线程池默认的handler是RejectedExecutionHandler = new AbortPolicy()。

其实线程池提供了4种:

  • AbortPolicy:直接抛异常
  • CallerRunsPolicy:直接调用线程任务的run方法执行,线程池不参与(虽然线程池不再接收,但是线程任务可以自己执行)。
  • DiscardPolicy:直接丢弃任务,也不报错也不干啥,神不知鬼不觉
  • DiscardOldestPolicy:将队列中最早的一条丢弃,再将当前任务添加到队列中。

线程池的队列:

ArrayBlockingQueue

ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按FIFO排序量。

LinkedBlockingQueue

LinkedBlockingQueue(可设置容量队列)基于链表结构的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,可以一直添加,直到资源耗尽。

DelayQueue

DelayQueue(延迟队列)是一个定时的延迟执行的队列。

SynchronousQueue

SynchronousQueue(直接提交队列),这种队列中不会保存任务,而是直接提交任务给线程处理。如果当前没有线程可用,则新建一个线程处理任务。

Executors:

concurrent 包提供了 Executors 工具类,利用它可以创建各种不同类型的线程池。
单线程的线程池:

固定线程数的线程池:

上面两种都是固定了线程数的,所以核心线程数和最大线程数一样多,因此,使用的队列是无边界的LinkedBlockingQueue,理论上可以一直放任务进去。

每接收一个任务就创建线程来执行,不往队列存放:

单线程具有周期调度功能的线程池:

多线程具有周期调度功能的线程池:

通过以上多种场景下使用的线程池可以发现,各种线程池,无非就是使用不同的参数而已。 Executors还支持传其他参数的线程池,当然,也可以自己定义。
在《阿里巴巴 Java 开发手册》中,其实是明确禁止使用 Executors 创建线程池,并要求开发者自己使用 ThreadPoolExector或 ScheduledThreadPoolExecutor进行创建。这样做是为了强制开发者自己传参,明确线程池的运行策略,使其对线程池的每个配置参数皆做到心中有数,以规避因使用不当而造成资源耗尽的风险。 下面,具体说一下ScheduledThreadPoolExecutor:
ScheduledThreadPoolExecutor:
ScheduledThreadPoolExecutor也是ThreadPoolExector的子类,核心参数都一样,只是做了增强:
延迟执行任务:

有两个方法,可以是Runnable或Callable任务。 在经过delay的时间后,才执行任务。

周期性执行任务:

两个方法:

scheduleAtFixedRate:按固定时间间隔执行,与任务本身的时间没有关系。 比如每隔5s执行一次当前任务。 但是它要求任务本身的执行时间要小于间隔时间,否则下一次开始的时候,上一次还没执行完。

scheduleWithFixedDelay: 也是按固定的时间间隔执行,但是与任务本身的时间有关系。 比如间隔2s执行一次,任务本身的执行时间是10s,那么下一次执行时间就是12s的时候。

  • initialDelay:说系统启动后,需要等待多久才开始执行第一次。
  • period:间隔执行时间。


延迟执行任务依靠的是 DelayQueue , 是 BlockingQueue的一种,其实现原理是二叉堆。当然这里没有直接使用,而是自己实现了一个DelayedWorkQueue。
而周期性执行任务是执行完一个任务之后,再把该任务扔回到任务队列中,如此就可以对一个任务
反复执行。

相关推荐
码观天工4 天前
从 Redis 客户端超时到 .NET 线程池挑战:饥饿、窃取与阻塞的全景解析
redis·线程池·线程饥饿
玩代码8 天前
Java线程池原理概述
java·开发语言·线程池
佛祖让我来巡山10 天前
【线程池饱和策略】线程池饱和策略及自定义方法
线程池·java线程池·线程池饱和策略
百锦再25 天前
.NET多线程任务实现的几种方法及线程等待全面分析
android·.net·线程·线程池·并发·thread·task
A22741 个月前
自定义线程池 4.0
java·线程池
阿维的博客日记1 个月前
说一下Java里面线程池的拒绝策略
java·线程池·拒绝策略
寒山李白1 个月前
Java中高并发线程池的相关面试题详解
java·开发语言·面试·高并发·线程池·多线程
LUCIAZZZ1 个月前
HikariCP数据库连接池原理解析
java·jvm·数据库·spring·springboot·线程池·连接池
Rocky4012 个月前
javaEE->多线程:线程池
java·运维·服务器·线程池·多线程·定时器
啾啾Fun2 个月前
【Java实战】低侵入的线程池值传递
java·线程池·ttl