线程池
线程是重量级对象
- 创建与销毁一个线程并不像创建一个对象那么简单,需要调用操作系统内核的 API,整个过程是一个偏重且耗时的操作
- 并发的线程数量很多,且执行时间很多的任务就结束,频繁的创建与销毁会大大降低系统效率
- 需要一种办法可以对线程进行重复利用,完成任务后并不被销毁,可以继续执行其他任务
线程池的设计思路
- 线程池是一种线程的使用模式,它为了降低线程使用过程中,频繁的创建和销毁所带来的资源消耗与代价
- 提前创建一定数量的线程,他们时刻准备着,在新任务到达后就开始执行;在完成任务后,再重新回来继续待命
- 线程池负责对线程统一分配、调优与监控
线程池的优点
- 避免了线程的重复创建与开销带来的资源消耗代价
- 提升了任务响应速度,任务到达时,直接选一个线程执行而无需等待线程的创建
- 提高线程的可管理性,线程的统一分配和管理,也方便统一的监控和调优
线程池的工作原理
- 在线程池的内部,我们维护了一个阻塞队列workQueue和一组工作线程,工作线程的个数可以在初始化线程池的时候来指定
- 用户提交Runnable任务给线程池,任务会被加入到workQueue中
- 线程池内部维护的工作线程会消费workQueue 中的任务并进行执行
如何使用Java 中的线程池
- Java提供的线程池相关的工具类Executors中,最核心的是ThreadPoolExecutor
- ThreadPoolExecutor的构造需要七大核心参数,定义了线程池的使用功能
线程池的核心参数
ThreadPoolExecutor(
int corePoolSize
int maximumPoolSize
long keepAliveTime ·
TimeUnit unit
BlockingQueue workQueue ·
hreadFactory threadFactory
RejectedExecutionHandler handler
)
(1)corePoolSize∶核心线程数
corePoolSize表示线程池保有的核心的线程数(最小的线程数)·核心线程会—直存活,即使这些线程处于空闲状态没有任务执行,他们也不会被销毁
- 把线程池类比为一个施工队,而线程就是施工队的工人_
- 有些时候很闲项目比较少,但是施工队也不能把工人都遣散,至少要留CorePoolSize个人坚守阵地
(2)maximumPoolSize∶线程池最大线程数量
- 当项目比较多的时候,施工队就需要增加工人,但是也不能无限制地加
- 最多就加到maximumPoolSize个人,当闲下来的时候,施工队就要遣散工人,但是至少保留 corePoolSize个人
(3)keepAliveTime & unit
- 上面提到施工队根据忙闲,项目多少来增减工人,那在编程世界里,如何定义忙和闲呢?
- 很简单,当线程池内部的线程数已经大于corePoolSize的时候,一个线程如果在一段时间内,都没有执行任务,说明很闲
- keepAliveTime和unit 就是用来定义这个"一段时间"的参数。也就是说,如果一个线程空闲了keepAliveTime & unit这么久,那么这个空闲的线程就要被回收了
(4)WorkQueue∶工作队列
新任务被提交到线程池,优先使用核心线程进行执行,核心线程没有空闲的情况下,任务会进入到此工作队列中,任务调度时再从队列中取出任务,由队列应该首先想到的是等待.
(5)threadFactory∶线程工厂
创建一个新线程时使用的工厂,通过这个工厂可以自定义如何创建线程,例如可以给线程指定一个有意义的名字
(6)handler∶ 拒绝策略
如果线程池中所有的线程(最大线程数)都在忙碌,并且工作队列也满了(前提是工作队列患有界队列),那么此时提交任务,线程池就会拒绝接收
至于拒绝的策略,可以通过 handler 这个参数来指定∶
- CallerRunsPolicy∶提交任务的线程自己去执行该任务。
- AbortPolicy∶默认的拒绝策略,直接丢弃任务,抛出RejectedExecutionException。
- DiscardPolicy∶直接丢弃任务,没有任何异常抛出。
- DiscardOldestPolicy∶丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
默认的拒绝策略要慎重使用。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略,并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用
使用线程池,需要注意异常处理的问题。任务在执行的过程中出现运行时异常,会导致执行任务的线程终止,最稳妥和简单的方案还是捕获所有异常并按需处理。
线程池在Java并发编程领域非常重要,很多大厂的编码规范都要求必须通过线程池来管理线程
对于核心参数的设置非常关键
如果在线程池中使用无界阻塞队列,会发生什么?
无界阻塞队列
- LinkedBlockingQueue默认的最大任务数量是Integer.MAXVALUE
- 如果线程池内的线程在获取到一个任务后,需要执行时间较长,会导致workQueue里积压的任务越来越多
- OOM
- 大量任务的积压导致机器的内存使用不停的飙升,最后导致OOM
- OOM,"Out Of Memory",俗称"内存用完了"。JVM因为没有足够的内存来为新的对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个error(非exception,因为这个问题已经严重到不足以被应用处理.
为什么会OOM?为什么会没有内存了呢?原因主要有两点∶
- 1)分配的少了∶比如虚拟机本身可使用的内存(一般通过启动时的VM参数指定)太少
- 2)应用用的太多,用完没释放浪费了,此时就会造成内存泄露或者内存溢出∶内存泄露和内存溢出经常同时出现
- 内存泄露∶申请使用完的内存没有释放,导致虚拟机不能再次使用该内存,此时这段内存就泄露了,因为申请者不用了,而又不能被虚拟机分配给别人用
- 内存溢出∶申请的内存超出了JVM能提供的内存大小,此时称之为溢出OOM
如果存在远程服务调用,使用无界阻塞队列,可能会出现什么情况?
只有一个线程的线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- corePoolSize和maxPoolSize设置为1,线程池内只有一个线程
- 通过LinkedBlockingQueue将任务进行排队,保证了串行执行所有任务,所有任务肯定是绝对顺序执行
无界阻塞队列不是说一定不能使用,一定要针对不同的场景来进行不同的分析
使用线程池时慎用无界队列,一般情况下针对特定场景才会进行使用
如果线上机器突然宕机,线程池阻塞队列中的请求怎么办?
任务丢失
线程机器宕机,必然导致内存中积压在阻塞队列的任务丢失
解决的思路
任务可能会丢失,怎么办?
- 不让它丢-保证系统永远都不会宕机(不切实际)
- 丢了之后再找回来
如何保证找回任务
- 在提交任务到线程池之前,先任务进行备份
- 如果发生宕机,内存中工作队列的任务会消失
- 机器重启后,从备份的数据中找回任务,重新提交重新执行
任务分阶段状态:将任务划分为多个状态∶未提交;已提交;已执行
思考下一个场景
如果一个任务在线程池内部执行完毕,还没来得及将任务改为已完成状态,这时候突然宕机,会发生什么?重复执行
任务仍处于已提交状态,重启之后会被重新提交,造成任务的重复执行,解决方案∶
- 所以不能只用已提交状态判断,还应该由具体业务,包括幂等性判断,辅助任务的状态判断是否应该重新提交此任务
- 如果任务执行的是数据库操作,可以采用事务,保证任务执行和任务状态更新同时完成,或者同时回滚
场景总结
任何时候,都可能会由宕机发生数据丢失∶
- 高可用
- 数据备份