第七章 - 共享模型之线程池
线程池 (重点)
池化技术
有很多, 比如线程池
、数据库连接池
、HTTP连接池
等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池提供了一种 限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
这里借用 《Java 并发编程的艺术》提到的来说一下使用线程池的好处:
-
降低资源消耗。
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。(创建的线程,实际最后要和操作系统的线程做映射,很消耗资源) -
提高响应速度。
当任务到达时,任务可以不需要等到线程创建就能立即执行。 -
提高线程的可管理性。
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
自定义线程池
- 阻塞队列中维护了由主线程(或者其他线程)所产生的的任务
- 主线程类似于生产者,产生任务并放入阻塞队列中
- 线程池类似于消费者,得到阻塞队列中已有的任务并执行
自定义线程池的实现步骤 :
- 步骤1:自定义拒绝策略接口
- 步骤2:自定义任务阻塞队列
- 步骤3:自定义线程池
- 步骤4:测试
- 阻塞队列
BlockingQueue
用于暂存来不及被线程执行的任务
- 也可以说是平衡生产者和消费者执行速度上的差异
- 里面的获取任务和放入任务用到了
生产者-消费者模式
- 线程池中对线程Thread进行了再次的封装,封装为了Worker
- 在调用 任务对象 (Runnable、Callable) 的run方法时,线程会去执行该任务,执行完毕后还会到阻塞队列中获取新任务来执行
- 线程池中执行任务的主要方法为
execute
方法
- 执行时要判断正在执行的线程数是否大于了线程池容量
ThreadPoolExecutor
线程池状态
ThreadPoolExecutor
使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
状态名称 | 高3位的值 | 描述 |
RUNNING | 111 | 接收新任务,同时处理任务队列中的任务 |
SHUTDOWN | 000 | 不接受新任务,但是处理任务队列中的任务 |
STOP | 001 | 中断正在执行的任务,同时抛弃阻塞队列中的任务 |
TIDYING | 010 | 任务执行完毕,活动线程为0时,即将进入终结阶段 |
TERMINATED | 011 | 终结状态 |
- 从数字上比较,
TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
线程池状态和线程池中线程的数量 由一个原子整型ctl来共同表示
- 使用一个数来表示两个值的主要原因是:可以通过一次CAS同时更改两个属性的值
- 获取线程池状态、线程数量以及合并两个值的操作
- 这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 CAS 原子操作进行赋值
线程池的属性
构造方法 (重点)
-
corePoolSize
:核心线程数 -
maximumPoolSize
:最大线程数
-
maximumPoolSize - corePoolSize = 救急线程数
- 注意 : 救急线程在没有空闲的核心线程和任务队列满了的情况才使用救急线程
-
keepAliveTime
:救急线程空闲时的最大生存时间 (核心线程可以一直运行) -
unit
:时间单位 (针对救急线程) -
workQueue
:阻塞队列(存放任务)
- 有界阻塞队列
ArrayBlockingQueue
- 无界阻塞队列
LinkedBlockingQueue
- 最多只有一个同步元素的
SynchronousQueue
- 优先队列
PriorityBlockingQueue
-
threadFactory
:线程工厂(给线程取名字) -
handler
:拒绝策略
工作方式
- 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
- 当线程数达到
corePoolSize (核心线程数)
并且没有线程空闲时,如果这时再加入任务,新加的任务会被加入workQueue (阻塞队列)
中排队,直到有空闲的线程。 - 如果队列选择了有界队列,那么任务超过了队列大小时,会创建
maximumPoolSize - corePoolSize (最大线程数 - 核心线程数)
数目的线程来救急。 - 如果线程数达到
maximumPoolSize (最大线程数)
仍然有新任务,这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现
- AbortPolicy 让调用者抛出
RejectedExecutionException
异常,这是默认策略 - CallerRunsPolicy 让调用者运行任务
- DiscardPolicy 放弃本次任务
- DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
- Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
- Netty 的实现,是创建一个新线程来执行任务
- ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
- PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
- 当高峰过去后,超过
corePoolSize(核心线程数)
的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit
来控制。
- 根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池
工作方式
- 当一个任务传给线程池以后,可能有以下几种可能
- 将任务分配给一个核心线程来执行
- 核心线程都在执行任务,将任务放到阻塞队列workQueue中等待被执行
- 阻塞队列满了,使用救急线程来执行任务
- 救急线程用完以后,超过生存时间(keepAliveTime)后会被释放
- 任务总数
大于
最大线程数(maximumPoolSize)与阻塞队列容量的最大值时(workQueue.capacity),使用拒接策略
拒绝策略
- 如果线程数达到
maximumPoolSize (最大线程数)
仍然有新任务,这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现
- 口诀法:拒中丢老调
- (线程池拒绝策略:中止策略、丢弃策略、弃老策略、调用者运行策略)
- 简单回答:
- 中止策略:无特殊场景。
- 丢弃策略:无关紧要的任务(博客阅读量)。
- 弃老策略:发布消息。
- 调用者运行策略:不允许失败场景(对性能要求不高、并发量较小)。
AbortPolicy 中止策略
:丢弃任务并抛出RejectedExecutionException异常。这是默认策略
- 这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
- 功能:当触发拒绝策略时,直接抛出拒绝执行的异常,中止策略的意思也就是打断当前执行流程.
- 使用场景:这个就没有特殊的场景了,但是有一点要正确处理抛出的异常。ThreadPoolExecutor中默认的策略就是AbortPolicy,ExecutorService接口的系列ThreadPoolExecutor因为都没有显示的设置拒绝策略,所以默认的都是这个。但是请注意,ExecutorService中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。
DiscardPolicy 丢弃策略
:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
- 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,本人的博客网站统计阅读量就是采用的这种拒绝策略。
- 功能:直接静悄悄的丢弃这个任务,不触发任何动作。
- 使用场景:如果你提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了。
DiscardOldestPolicy 弃老策略
:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- 此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
- 功能:如果线程池未关闭,就弹出队列头部的元素,然后尝试执行
- 使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。基于这个特性,想到的场景就是,发布消息和修改消息,当消息发布出去后,还未执行,此时更新的消息又来了,这个时候未执行的消息的版本比现在提交的消息版本要低就可以被丢弃了。因为队列中还有可能存在消息版本更低的消息会排队执行,所以在真正处理消息的时候一定要做好消息的版本比较。
CallerRunsPolicy 调用者运行策略
:由调用线程处理该任务。
- 功能:当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理。
- 使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了。
newFixedThreadPool
内部调用的构造方法
- 特点
- 核心线程数 = 最大线程数(没有救急线程被创建),因此也无需超时时间
-
阻塞队列是无界的,可以放任意数量的任务
- 适用于任务量已知,相对耗时的任务
这个是
Executors类
提供的工厂方法来创建线程池!Executors
是Executor 框架的工具类!
- 线程池大小为2,3个任务,t1线程执行完1后,就去执行3了
- 创建出的线程默认都为非守护线程,main线程执行完也没有结束
newCachedThreadPool
内部构造方法
- 没有核心线程,最大线程数为Integer.MAX_VALUE,所有创建的线程都是救急线程 (可以无限创建),空闲时生存时间为60秒
- 阻塞队列使用的是SynchronousQueue
- SynchronousQueue是一种特殊的队列
- 没有容量,没有线程来取是放不进去的 (一手交钱、一手交货)
- 只有当线程取任务时,才会将任务放入该阻塞队列中
- 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。
- 适合任务数比较密集,但每个任务执行时间较短的情况
SynchronousQueue 演示
- 用newCachedThreadPool( )创建出线程池,其阻塞队列的实现是
SynchronousQueue
。 - 线程池初始的最大线程数是Integer的最大值,但是全是救急线程,且线程是懒惰初始化的(即一开始不会真的全部创建出来,但是用到了就能创建这么多)
- 然后阻塞队列的容量为空,没有线程来取就存放不进去,起到了一个缓冲作用,根本也无需阻塞,因为救急线程相当于是没有上限的,很快就能来把你这个任务取走。
- 等到线程池里的线程任务执行完成后,空闲1分钟后就会释放之前创建的救急线程。
newSingleThreadExecutor
内部构造方法
使用场景:
- 希望多个任务排队执行。线程数固定为 1;任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
区别:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而
newSingleThreadExecutor
线程池还会新建一个线程,保证池的正常工作 -
Executors.newSingleThreadExecutor()
线程个数始终为1,不能修改
-
FinalizableDelegatedExecutorService
应用的是装饰器模式,只对外暴露了ExecutorService
接口,因此不能调用ThreadPoolExecutor
中特有的方法
-
Executors.newFixedThreadPool(1)
初始时为1,以后还可以修改
- 对外暴露的是
ThreadPoolExecutor
对象,可以强转后调用setCorePoolSize
等方法进行修改
代码示例
- 线程1挂掉后,又新建了一个线程2来执行任务,始终保证线程池中有一个可用的线程。
Executors 返回线程池对象的弊端如下 (重点)
注意: Executors 返回线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE (无界阻塞队列) , 可能堆积大量的请求,从而导致 OOM。
- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
- 建议使用
ThreadPoolExecutor
来创建线程
避免上面的措施 :
使用有界队列,控制线程创建数量。
执行/提交任务 execute/submit
execute( )方法
- 传入一个Runnable对象,执行其中的run方法
- 源码解析
- 其中调用了 **addWoker( )**方法,再看看看这个方法
submit( )方法
- 传入一个Callable对象,用Future来捕获返回值
invokeAll
invokeAny
关闭线程池 shutdown( )
shutdown( )
- 将线程池的状态改为 SHUTDOWN
- 不再接受新任务,但是会将阻塞队列中的任务执行完
shutdownNow( )
- 将线程池的状态改为 STOP
- 不再接受新任务,也不会在执行阻塞队列中的任务
- 会将阻塞队列中未执行的任务返回给调用者
- 并用 interrupt 的方式中断正在执行的任务
其它方法
代码示例
shutdown输出
shotdownNow输出
异步模式之工作线程
定义
- 让有限的工作线程(Worker Thread)来轮流异步处理无限多的任务。也可以将其归类为
分工模式
,它的典型实现就是线程池,也体现了经典设计模式中的享元模式。 - 例如,海底捞的服务员(线程),轮流处理每位客人的点餐(任务),如果为每位客人都配一名专属的服务员,那么成本就太高了(对比另一种多线程设计模式:Thread-Per-Message)
- 注意,不同任务类型应该使用不同的线程池,这样能够避免饥饿,并能提升效率
- 例如,如果一个餐馆的工人既要招呼客人(任务类型A),又要到后厨做菜(任务类型B)显然效率不咋地,分成服务员(线程池A)与厨师(线程池B)更为合理,当然你能想到更细致的分工
饥饿
固定大小线程池就会有饥饿现象
- 两个工人是同一个线程池中的两个线程
- 他们要做的事情是:为客人点餐和到后厨做菜,这是两个阶段的工作
- 客人点餐:必须先点完餐,等菜做好,上菜,在此期间处理点餐的工人必须等待
- 后厨做菜:没啥说的,做就是了
- 比如工人A 处理了点餐任务,接下来它要等着 工人B 把菜做好,然后上菜,他俩也配合的蛮好
- 但现在同时来了两个客人,这个时候工人A 和工人B 都去处理点餐了,这时没人做饭了,饥饿
饥饿示例
只有一位客人,线程1负责点餐和上菜,线程2负责做菜
当把注释取消时,相当于来了两位客人,线程1和线程2都去点餐了,就没人在做菜了,这时就出现了饥饿现象
饥饿解决
解决方法可以增加线程池的大小,不过不是根本解决方案,还是前面提到的,
不同的任务类型,采用不同的线程池
线程池中线程设置多少为好?
- 过小会导致程序不能充分地利用系统资源、容易导致饥饿
- 过大会导致更多的线程上下文切换,占用更多内存
CPU 密集型运算
通常采用 **cpu 核数 + 1
**能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费
I/O 密集型运算
CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程 RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。
经验公式如下
- 例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式
- 例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
任务调度线程池
- 在『任务调度线程池』功能加入之前,可以使用
java.util.Timer
来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程
来调度,因此所有任务都是串行
执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
ScheduledExecutorService (重点)
ScheduledExecutorService 中 schedule方法的使用
- 两个线程执行互不干扰
- 当然,如果线程池大小为1,任务之间仍然是串行执行
ScheduledExecutorService 中 scheduleAtFixedRate方法的使用
如果任务执行时间超过了间隔时间
- 输出分析:一开始,延时 1s,接下来,由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s
ScheduledExecutorService 中 scheduleWithFixedDelay方法的使用
- 输出分析:一开始,延时 1s,scheduleWithFixedDelay 的间隔是
上一个任务结束 + 延时 = 下一个任务开始
,所以间隔都是 3s - 整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务。
eg:如何让每周四 18:00:00 定时执行任务?
正确处理执行任务异常
方法1:主动捉异常
方法2:使用 Future
- 使用Future接收返回值,如果正常则接收Callable接口返回值
- 有异常的话,就会捕捉异常
Tomcat 线程池
Tomcat 在哪里用到了线程池呢?
- LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲
- Acceptor 只负责【接收新的 socket 连接】
- Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】
- 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理
- Executor 线程池中的工作线程最终负责【处理请求】
体现了不同的线程池做不同的工作
Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同
- 如果总线程数达到 maximumPoolSize
- 这时不会立刻抛 RejectedExecutionException 异常
- 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常
源码 tomcat-7.0.42
- TaskQueue.java
- Connector 配置
- Executor 线程配置
守护线程的意思就是线程会随着主线程的结束而结束
- 下图有点错误,
提交任务<核心线程
, 应该直接交给核心线程执行。
Fork/Join (熟悉)
概念
- Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
- 所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
- Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
- Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
使用
- 提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下面定义了一个对 1~n 之间的整数求和的任务
图示效果
改进
图示结果