万字长文详解Java线程池

为什么是需要使用线程池?

如果直接创建线程,存在以下问题:
1、反复创建线程系统开销比较大,每个线程的创建和销毁都需要时间,如果线程执行的任务比较简单,那么创建销毁线程占用资源比线程本身执行任务销毁的资源还要多。
2、过多的线程会消耗更多的内存,也会造成过多的上下文切换,使得系统不稳定。
使用线程池优点
1、线程池可以控制线程生命周期的开销问题,同时加快响应速度。线程池的线程是可以复用的,通过少量的线程执行大量的任务。
2、线程池可以统筹内存和CPU的使用,避免资源使用不当。线程池可以根据配置和任务数量灵活控制线程数量。
3、线程池可以统一管理资源。

线程池各个参数的含义

属性名字作用
corePoolSize核心线程数当线程池运行线程少于corePoolSize时,将创建一个新的线程来处理请求,即使其他线程处于空闲状态
threadFactory线程工厂用于创建线程的工厂
workQueue队列用于保留任务并移交工作线程的阻塞队列
maximunPoolSize最大线程数线程池允许开始最大线程数
handler拒绝策略在线程池添加任务时候,两种情况下触发拒绝策略:1、线程池运行状态不是RUNNING;2、线程池达到最大线程数,并且阻塞队列已经满。
keepAliveTime保持存活时间当线程池线程数大于核心线程数时,多余的线程空闲时间超过keepAliveTime时会被终止。

线程创建时机

corePoolSize,maximumPoolSize

corePoolSize 指的是核心线程数,线程池初始化时线程数默认为 0,当有新的任务提交后,会创建新线程执行任务,如果不做特殊设置,此后线程数通常不会再小于 corePoolSize ,因为它们是核心线程,即便未来可能没有可执行的任务也不会被销毁。随着任务量的增加,在任务队列满了之后,线程池会进一步创建新线程,最多可以达到 maximumPoolSize 来应对任务多的场景,如果未来线程有空闲,大于 corePoolSize 的线程会被合理回收。所以正常情况下,线程池中的线程数量会处在 corePoolSizemaximumPoolSize 的闭区间内。

线程池的几个特点

  • 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加线程。
  • 线程池只有在任务队列填满时才创建多于 corePoolSize 的线程,如果使用的是无界队列(例如 LinkedBlockingQueue),那么由于队列不会满,所以线程数不会超过 corePoolSize
  • 通过设置 corePoolSizemaximumPoolSize 为相同的值,就可以创建固定大小的线程池。
  • 通过设置 maximumPoolSize 为很高的值,例如 Integer.MAX_VALUE,就可以允许线程池创建任意多的线程。

线程池状态

RUNNING:接受新任务并处理排队的任务。
SHUTDOWN:不接受新任务,但处理排队的任务。
STOP:不接受新任务,不处理排队的任务,并且中断进行中的任务。
TIDYING:所有的任务都已经终止,workerCount为零,线程转换到TIDYING状态将运行terminated()钩子方法。
TERMINATEDterminated()已经完成。

线程池状态转换

线程池有哪些拒绝策略

拒绝时机

1、线程池不在RUNNING状态,线程池调用shutdown等方法关闭线程池后,即便线程池内部依然存在没有执行完的任务正在执行,但是线程池已经关闭,此时再向线程池内提交任务,就会遭到拒绝。
2、线程池没有能力继续处理新提交的任务。线程池达到最大线程数,且队列已经满的情况。

4种拒绝策略


AbortPolicy:在拒绝任务时,会抛出异常。(异常:RejectedExecutionExceptionRuntimeException
DiscardPolicy:当提交新任务时候,直接抛弃掉,不做任何通知。
DiscardOldestPolicy:当提交新任务时候,丢弃掉存活时间长的队列头任务,腾出空间给新的任务.
CallerRunsPolicy:当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。

阻塞队列

ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出对元素进行排序。
LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于ArrayBlockingQueueExecutors.newFixedThreadPool使用了该队列。
SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。Executors.newCachedThreadPool使用了该队列。
PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序。元素的优先级是通过自然顺序或 Comparator 来定义的。

6 种常见的线程池?

FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadExecutor、SingleThreadScheduledExecutor、ForkJoinPool

FixedThreadPool

FixedThreadPool 是一个固定大小的线程池。这意味着它在创建时会同时创建固定数量的线程,并且这些线程会一直存在,直到线程池被关闭。
关键特性
固定线程数量:一旦创建,线程池中的线程数量就不会再改变,除非线程由于某种原因终止,那么线程池可能会创建新线程来替换它们。
线程复用FixedThreadPool 会重用之前构造的线程,而不是每次执行任务时都创建新线程,这样可以减少线程创建和销毁的开销。
任务队列:当所有线程都在忙碌时,新提交的任务会被放入一个工作队列中等待执行。这个队列可以是无界的,也可以是有界的,具体取决于线程池的构造方式。
线程池管理FixedThreadPool 提供了方法来管理线程池,比如关闭线程池、获取当前线程数量、获取已完成的任务数量等。
异常处理:如果任务执行时抛出未检查的异常,那么这个线程将会终止,线程池会创建新线程来替换它。
公平性:可以通过构造函数参数来指定线程池是否应该按照任务提交的顺序来执行它们(公平性)。默认情况下,FixedThreadPool 是非公平的,即线程池中的线程可能会以非 FIFO 的顺序执行任务。

CachedThreadPool

CachedThreadPool 是一种可根据需要创建新线程的线程池,这些线程在执行完任务后不会立即销毁,而是会被缓存起来供以后使用。如果线程在指定的时间内没有被分配新任务,那么它们将被终止并从缓存中移除。
关键特性
线程缓存CachedThreadPool 会尝试重用已有的线程,而不是每次都创建新线程。
动态线程创建:当现有线程不足以处理新任务时,线程池会创建新线程。
线程回收:如果一个线程在指定的空闲时间内没有被使用,它将被终止。
无界线程数量:理论上,CachedThreadPool 可以创建的线程数量没有上限,但实际上受限于系统资源和构造函数中指定的保持空闲线程存活时间的上限。
保持活动时间:可以设置一个保持活动时间,在这个时间内如果线程空闲,它将被终止。
非核心线程:由于它的特性,CachedThreadPool 更适合用于短生命周期的异步任务。

ScheduledThreadPool

ScheduledThreadPool 允许用户在给定的延迟后运行任务,或者定期重复执行任务。这个线程池能够满足需要任务调度的场景,比如延时执行、定时执行等。
关键特性
延时执行:可以安排任务在一定的延迟之后执行。
周期性执行:可以安排任务按照指定的周期重复执行。
线程复用ScheduledThreadPool 会重用之前构造的线程来执行新的任务,减少线程创建和销毁的开销。
灵活性:可以灵活地安排单次执行或多次执行的任务。
固定线程数量:创建时可以指定线程池的大小,这个大小是固定的。

SingleThreadExecutor

SingleThreadExecutor 创建一个单线程的执行器。这个线程池中只有一个线程工作,所有提交给该执行器的任务都会按照顺序依次执行。
关键特性
单线程执行:只有一个线程在工作,任务会按照它们被提交的顺序依次执行。
任务队列:如果线程正在执行任务,后续提交的任务会被放入队列中等待执行。
线程复用:SingleThreadExecutor 会重用同一个线程来执行多个任务。
避免线程创建开销:由于只有一个线程,避免了频繁创建和销毁线程的开销。
错误传递:如果一个任务执行失败,后续任务将不会执行,线程池会关闭。

SingleThreadScheduledExecutor

SingleThreadScheduledExecutorScheduledExecutorService 接口的一个具体实现。与 SingleThreadExecutor 类似,SingleThreadScheduledExecutor 也只有一个工作线程,但它专门用于延迟执行或定期执行任务。
关键特性
单线程:只有一个线程用于执行所有任务,确保任务按顺序执行。
定时任务:可以安排任务在给定的延迟后执行,或者定期执行。
可调度性:提供了灵活的调度功能,包括单次执行、固定频率执行和固定延迟执行。
线程复用:与 SingleThreadExecutor 一样,它会重用同一个线程来执行多个任务。
错误处理:如果任务执行过程中抛出异常,默认情况下,后续的任务将不会执行,线程池会关闭。可以通过适当的异常处理逻辑来避免这种情况。

ForkJoinPool

ForkJoinPool 是 一个专门用于分治任务模型的线程池实现。它特别适合于可以递归分解为更小任务的问题,比如大规模数值计算、图像处理、大数据处理等。
关键特性
工作窃取算法ForkJoinPool 使用工作窃取算法来均衡地分配任务给线程。每个线程维护一个双端队列,用于存储任务。线程可以从队列的两端出队任务执行,但通常从自己的那一端出队,而从其他线程的队列另一端窃取任务。
分治任务模型:适合于可以递归分解为更小任务的问题。任务被分解(fork)成更小的任务并行执行,最终结果通过合并(join)操作得到。
轻量级任务ForkJoinPool 特别适合执行大量小任务,这些任务可以快速执行并完成。 延迟执行:任务通常在提交后不会立即执行,而是延迟到工作线程可用时才执行。
可扩展性ForkJoinPool 可以很好地扩展到多核处理器,并且可以根据系统的处理器数量动态调整线程数量。 专用任务类ForkJoinPool 使用 RecursiveTaskRecursiveAction 类来表示有返回值和无返回值的任务。

线程池线程数多少合适?

线程池处理的任务通常分为cpu密集型任务、IO密集型任务。

cpu密集型任务核心线程数设置

加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务
核心线程数: 这样的任务最佳的线程数为 CPU 核心数的 1~2 倍
原因: 因为计算任务非常重,会占用大量cpu资源,此时每个核心线程都是满负荷工作。设置过多的线程数会造成不必要的上下文切换。

IO密集型任务核心线程数设置

数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。
核心线程数: 最大线程数一般会大于 CPU 核心数很多倍,线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)。例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个
原因:因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。

使用线程池队列需要注意什么?

有界队列:需要注意线程池满后。被拒绝的任务如何处理。
无界队列:如果提交任务大于线程池处理任务的速度时候 ,可能导致内存溢出。

如何根据实际需要,定制自己的线程池?

定制自己的线程池需要考虑多个因素,包括任务类型、并发量、资源限制和业务需求。定制线程池时需要考虑的关键步骤和参数:
确定核心线程数(corePoolSize):核心线程数是指线程池中始终保持激活的线程数量,即使它们处于空闲状态。对于CPU密集型任务,核心线程数通常设置为处理器核心数的1至2倍。对于IO密集型任务,可以设置更多的线程,因为线程会在等待IO操作时释放CPU。
确定最大线程数(maximumPoolSize):最大线程数是指线程池中允许的最大线程数量。如果任务数量波动很大,或者有大量短期任务,可能需要设置一个较高的最大线程数。
选择阻塞队列:阻塞队列用于存储等待执行的任务。常见的阻塞队列有 LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue 等。队列的选择会影响线程池如何管理等待任务和内存使用。
设置线程工厂(threadFactory):线程工厂用于创建新线程,可以自定义线程的名称、优先级、守护进程状态等。自定义线程工厂可以帮助你更好地监控和调试线程池。
设置拒绝策略(rejectedExecutionHandler):当任务太多,无法被线程池及时处理时,需要一个拒绝策略来处理新任务。常见的拒绝策略包括AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy。也可以实现自定义的拒绝策略。
设置保持活动时间(keepAliveTime):非核心线程在空闲状态下等待新任务的最长时间。如果任务是周期性的,可以设置一个合适的保持活动时间。
设置时间单位(timeUnit):keepAliveTime 参数的时间单位,如 TimeUnit.SECONDS、TimeUnit.MILLISECONDS 等。
考虑是否允许核心线程超时:如果允许,即使核心线程也会在空闲时被终止。
考虑工作线程的优先级:可以为工作线程设置不同的优先级,以影响任务的执行顺序。
考虑是否需要单线程执行:如果任务之间需要严格的顺序执行,或者有共享状态需要严格同步,可能需要使用单线程执行器。

线程只能在任务到达时才启动吗?

默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是我们可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。

核心线程怎么实现一直存活?

抛出异常返回特殊值一直阻塞超时退出
插入add(e)offer(e)put(e)offer(e,time,unit)
移除remove()poll()take()poll(time,unit)
检查element()peek()不可用不可用

核心线程在获取任务时,通过阻塞队列的 take() 方法实现的一直阻塞(存活)。

非核心线程如何实现在 keepAliveTime 后死亡?

原理同上,也是利用阻塞队列的方法,在获取任务时通过阻塞队列的 poll(time,unit) 方法实现的在延迟死亡。

非核心线程能成为核心线程吗?

虽然我们一直讲着核心线程和非核心线程,但是其实线程池内部是不区分核心线程和非核心线程的。只是根据当前线程池的工作线程数来进行调整,因此看起来像是有核心线程于非核心线程。

如何终止线程池?

终止线程池主要有两种方式:
shutdown:“温柔”的关闭线程池。不接受新任务,但是在关闭前会将之前提交的任务处理完毕。
shutdownNow:“粗暴”的关闭线程池,也就是直接关闭线程池,通过 Thread#interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。

线程池ctl

ctl怎么设计的?

ctl是打包两个概念字段的原子整数。
workerCount:指示线程的有效数量。
runState:线程池状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
ctl int类型、32位。高3位为runState的,低29位为workerCount。
例如,当我们的线程池运行状态为 RUNNING,工作线程个数为3,
则此时 ctl 的原码为:1010 0000 0000 0000 0000 0000 0000 0011

为什么要这样设计?

runStateworkerCount 是线程池正常运转中的2个最重要属性,线程池在某一时刻该做什么操作,取决于这2个属性的值。
因此无论是查询还是修改,我们必须保证对这2个属性的操作是属于“同一时刻”的,也就是原子操作,否则就会出现错乱的情况。如果我们使用2个变量来分别存储,要保证原子性则需要额外进行加锁操作,这显然会带来额外的开销,而将这2个变量封装成1个 AtomicInteger 则不会带来额外的加锁开销,而且只需使用简单的位操作就能分别得到 runStateworkerCount
由于这个设计,workerCount 的上限 CAPACITY = (1 << 29) - 1,对应的二进制原码为:0001 1111 1111 1111 1111 1111 1111 1111(不用数了,29个1)。
通过 ctl 得到 runState,只需通过位操作:ctl & ~CAPACITY
~(按位取反),于是“~CAPACITY”的值为:1110 0000 0000 0000 0000 0000 0000 0000,只有高3位为1,与 ctl 进行 & 操作,结果为 ctl 高3位的值,也就是 runState
通过 ctl 得到 workerCount 则更简单了,只需通过位操作:c & CAPACITY

其它

关注公众号【 java程序猿技术】获取八股文系列文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后端马农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值