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: 首先将队列中队头任务丢弃掉(不抛出任何异常),然后将当前任务加入到阻塞队列中

后续

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

Java运行GroovyShell时,可以配置线程池的大小以提高程序的性能和效率。线程池的大小决定了可以同时执行的线程数量,过大的线程池会导致资源浪费,而过小的线程池则可能导致线程阻塞和程序运行缓慢。 根据经验和实践,以下是一些建议的线程池大小配置: 1. 根据CPU核心数进行配置:可以根据当前计算机的CPU核心数来配置线程池大小。一般来说,将线程池的大小设置为CPU核心数的2倍或4倍是一个合理的选择。 2. 考虑任务的类型和复杂性:如果要执行的任务是密集计算型的,那么可以增大线程池的大小以充分利用系统资源。而如果任务是I/O密集型的,那么较小的线程池大小可能更为适当,因为线程可能在等待I/O操作完成时被阻塞。 3. 考虑内存和系统资源:大型的线程池会消耗更多的内存和系统资源,因此在配置线程池大小时需要考虑服务器的内存和系统资源限制。确保线程池的大小不会超过系统所能够承受的范围。 4. 考虑任务的排队情况:线程池的大小也要考虑任务的排队情况。如果任务队列中的任务很多,可以适当增大线程池的大小以加快处理速度。而如果任务队列中的任务相对较少,可以减小线程池的大小以节省资源。 综上所述,对于Java运行GroovyShell时的线程池大小配置建议是根据CPU核心数选择2倍或4倍作为初始线程池大小,并根据任务类型、内存和系统资源以及任务排队情况进行适当调整。这样可以在充分利用系统资源的同时,避免出现资源浪费和性能瓶颈。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值