Java线程池到底是怎么运行的?(一)

7be7ae1650027ea430f484867e0a427a.png

前言

由于线程池相关核心内容较长,为了方便大家阅读,会将内容分为上下两篇,上篇主要讲解如何使用,以及线程池的基本原理;下篇深入源码展开分析验证原理的正确性。

一、线程池的创建与使用?

方案1:使用Executors工具类创建 (强烈不推荐)

// 初始化一个核心线程数为0、最大线程数为Integer.MAX_VALUE,队列为SynchronousQueue,线程空闲时间为60s的线程池
ExecutorService executorService = Executors.newCachedThreadPool();

存在的问题:

  1. SynchronousQueue是一个无缓冲的阻塞队列,所以提交任务后,必须有线程去执行,否则会一直阻塞住

  2. 线程池的线程数量会根据任务数量动态调整,如果有大量的任务,可能会导致CPU飙升,甚至OOM

  3. 线程池的线程数量会根据任务数量动态调整,所以任务执行完成后,线程池中的线程数量会一直减少,最终为0,所以线程池中的线程数量会一直变化

// 初始化一个核心线程数为1、最大线程数为1,队列为LinkedBlockingQueue,线程空闲时间为0s的线程池
ExecutorService executorService1 = Executors.newFixedThreadPool(1);

存在的问题:

  1. LinkedBlockingQueue是一个有界队列,所以提交任务后,不会阻塞,但是会一直往队列中提交任务,直到队列元素达到Integer.MAX_VALUE,导致OOM

// 初始化一个核心线程数为1、最大线程数为1,队列为LinkedBlockingQueue,线程空闲时间为0s的线程池
ExecutorService executorService2 = Executors.newSingleThreadExecutor();

存在的问题:

  1. LinkedBlockingQueue是一个有界队列,所以提交任务后,不会阻塞,但是会一直往队列中提交任务,直到队列元素达到Integer.MAX_VALUE,导致OOM

/// 初始化一个核心线程数为1、最大线程数为Integer.MAX_VALUE,队列为DelayQueue,线程空闲时间为0s的线程池
ScheduledExecutorService executorService3 = Executors.newScheduledThreadPool(1);

存在的问题:

  1. DelayQueue是无界的,这意味着它可以无限地添加元素,直到系统内存耗尽。

  2. 最大线程数为Integer.MAX_VALUE,这意味着线程池可以无限地创建线程,直到系统内存耗尽。

方案2:自定义线程池 不太建议

// 初始化一个核心线程数为1、最大线程数为10,队列为ArrayBlockingQueue,线程空闲时间为60s的线程池
ThreadPoolExecutor poolExecutor =
    new ThreadPoolExecutor(1, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(500));

存在的问题:

  1. 线程参数无法正确设置,业界是否存在一劳永逸的办法?很遗憾:答案是没有;以下截图来源于美团线程池分析文章3c670027642a0c9ca7f0280d53cc9c9b.png

  2. 线程池在生产环境中运行过程中的状态无法可视化监控,出现问题无法快速感知

  3. 系统发出异常告警时,无法快速调整线程池参数(当然ThreadPoolExecutor本身提供口子用于动态修改相关参数,但需要开发者开发一整套工具来方便调整参数)9bbfe808bdb156db3b12e59cf08a83e8.png

方案3:开源的动态线程池 推荐

当公司内没有能力或资源专门开发线程池组件时,开源的Java线程池组件,其基本思路是基于动态线程池去实现的,这些开源的线程池也有很多公司在使用,同时版本迭代快,社区活跃度也比较高。

  1. DynamicTp,具备动态调参、通知报警、运行监控等功能,可以解决核心参数无法一步设置到合理值的窘境。官网地址: https://dynamictp.cn/ , Github: https://github.com/dromara/dynamic-tp

  2. Hippo4j,具备动态变更、自定义报警、运行监控等功能,可以解决核心参数无法一步设置到合理值的窘境。官网地址: https://hippo4j.cn/zh/ , Github: https://github.com/opengoofy/hippo4j

  3. Spring Boot Actuator,Spring内置的一个监控功能,严格意义上不太算线程池组件。官网地址:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html

方案4:基于动态线程池自研的线程池组件 强烈推荐

公司架构团队专门针对自身业务场景而研发的动态线程池,比如在动态线程池的基础上,开发了亲缘性线程池等。

小结:简单分析后可以看出,实际生产环境中线程池使用优先级应该是:公司自研线程池组件 > 三方开源线程池组件 > 自定义线程池 > 坚决不使用JDK自带的线程池组件。

二、线程池的基本工作原理

到此,我们已经掌握了如何创建和使用线程池了,下面我们来看看线程池是怎么运转的。整个线程池其实可以看做是一个生产者/消费者模型,任务源源不断生产出来提交到线程池,阻塞队列作为缓冲区暂存任务,线程池内的线程则不断从缓冲区获取任务进行消费;通过池化的思想来避免线程重复重建、销毁带来的性能损耗。

55a8f5676507be7b63b325dcd3e65603.png

  1. 首先初始化一个线程池,注意此时仅创建了一个线程池空壳子,并没有创建出来任何线程

  2. 当有任务提交到线程池执行,若活跃的线程数小于核心线程数,则创建一个新的线程来处理提交上来的任务

  3. 若活跃的线程数大于等于核心线程数,则将提交的任务暂存到阻塞/非阻塞队列中,等候执行

  4. 当任务还在源源不断的提交上来,直到塞满队列后,此时若活跃线程数小于最大核心线程数,则继续创建新的线程来处理新提交上来的任务

  5. 若当前活跃线程数超过最大核心数后,新提交上来的任务就需要执行相应的拒绝策略了。i) CallerRunsPolicy:检查当前线程池状态若还在正常运行时,则直接使用提交任务的线程去执行该任务 ii) AbortPolicy: 将新提交到线程池的任务直接丢弃掉,并抛出RejectedExecutionException异常,交由业务代码进行处理 iii) DiscardPolicy: 将新提交到线程池的任务直接丢弃掉,但不会抛出异常让外界感知,一般线上业务不会也不允许使用该策略 iv) DiscardOldestPolicy: 首先将队列中队头任务丢弃掉(不抛出任何异常),然后将当前任务加入到阻塞队列中

后续

本篇文章主要讲解了线程池如何创建和使用,以及从理论层面初步分析了线程池的基本工作过程,下篇文章我们开始进一步深入到源码来对理论进行验证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值