【并发编程-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。
而周期性执行任务是执行完一个任务之后,再把该任务扔回到任务队列中,如此就可以对一个任务
反复执行。

相关推荐
Winston Wood19 小时前
Java线程池详解
java·线程池·多线程·性能
雪碧聊技术4 天前
多线程4:线程池、并发、并行、综合案例-抢红包游戏
java·线程池·多线程·并发·并行·复用线程
阑梦清川23 天前
JavaEE初阶---多线程(五)---定时器/线程池介绍
java·java-ee·线程池·定时器
七禾页话1 个月前
自定义线程池
java·线程池
HKJ_numb11 个月前
多线程——线程池
java·设计模式·线程池·多线程·简单工厂模式·拒绝策略
好个秋1 个月前
java线程池bug的一些思考
java·线程池
yezipi耶不耶1 个月前
Java线程池知识点梳理
java·开发语言·线程池·多线程
向阳12181 个月前
java并发之线程池使用
java·高并发·线程池·多线程
JavaGuide1 个月前
深信服后端开发岗校招面经,挂在了二面!
分布式·哈希算法·线程池·代码规范·分布式id·系统设计·虚拟线程·加密算法·rdb·密码加密·guice
GGBondlctrl1 个月前
【JavaEE初阶】深入理解线程池的概念以及Java标准库提供的方法参数分析
java·开发语言·java-ee·线程池·拒绝策略·线程池的模拟