线程池面试点

本文详细探讨了使用线程池的原因,包括资源管理和性能提升,并解释了线程池的核心属性如线程工厂、核心线程数、队列策略等。还介绍了线程池的状态、拒绝策略和不同类型的队列。最后讨论了线程池大小的合理配置策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么要用线程池?

如果我们在方法中直接new一个线程,当这个方法被调用频繁时就会创建很多线程,不仅会消耗系统资源,还会降低系统的稳定性.总的来说使用线程池可以:

1.降低资源消耗,通过重复利用已创建的线程,降低线程创建和销毁造成的损耗

2.提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行

3.增加线程的可管理性,线程是稀缺资源,使用线程池可以进行统一分配,调优和监控

线程池的核心属性?

threadFactory(线程工厂):用于创建线程的工厂

corePoolSize(核心线程数):当线程池运行的线程少于corePoolSize时,将创建一个新线程来处理请求,即使其他工作线程处于空闲状态.

workQueue(队列):用于保留任务并移交给工作线程的阻塞队列

maximumPoolSize(最大线程数):线程池允许开启的最大线程数

handle(拒绝策略):往线程池添加任务时,将在下面两种情况下触发决绝策略:

1.线程池运行状态不是running

2.线程池已经达到最大线程数,并且阻塞队列已满时

keepAliveTime(保持存活时间):如果线程池当前线程超过corePoolSize,则多余的线程空闲时间超过keepAliveTime时会被终止.

线程池的运作流程

 线程池中的各个状态分别代表什么含义?

线程池有5个状态:

RUNNING:接受新任务并处理排队的任务

SHUTDOWN:不接受新任务,但处理排队的任务

STOP:不接受新任务,不处理排队的任务,并中断正在进行的任务

TIDYING:所有任务已终止,workerCount为零,线程转换到TIDYING状态将运行terminated()钩子方法

TERMINATED:terminated()已完成.

线程池有哪些队列?

常见的阻塞队列有以下几种:

ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出对元素进行排序,

LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于ArrayBlockingQueue.Executors.newFixedThreadPool使用了该队列.

SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制.要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素.如果没有线程等待,并且线程池的大小小于最大值,那么线程将创建一个线程,否则根据拒绝策略,这个任务将被拒绝.使用直接移交将更高效,因为任务会直接移交给执行他的线程,而不是被放在队列中,然后有工作线程从队列中提取任务.只有当线程池是无界的或者可以拒绝任务时,该队列才有实际值.

Executors.newCachedThreadPool使用了该队列

PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序.元素的优先级是通过自然顺序或comparator来定义的.

使用队列有什么需要注意的吗?

使用有界队列时,需要注意线程池满了之后,被拒绝的任务如何处理

使用无界队列时,需要注意如果任务的提交速度大于线程池的处理速度,可能会导致内存泄漏.

线程池有哪些拒绝策略?

常见的有:

AbortPolicy:中止策略.默认的拒绝策略,直接抛出RejectedExecutionException.调用者可以捕获这个异常,然后根据需求编写自己的处理代码

DiscardPolicy:抛弃策略.什么都不做,直接抛弃被拒绝的任务.

DiscardOldestPolicy:抛弃最老策略,抛弃阻塞队列中最老的任务,相当于就是队列中下一个被执行的任务,然后重新提交被拒绝的任务,如果阻塞队列是一个优先队列,那么"抛弃最旧的"策略将导致抛弃优先级最高的任务,因此最好不要将该策略和优先级队列放在一起使用.

CallerRunsPolicy:调用者运行策略.在调用者线程中执行该任务.该策略实行了一种调节机制,该策略既不会抛弃任务也不会抛出异常,而是将任务回退到调用者(调用线程池执行任务的主线程),由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得线程池有时间来处理完正在执行的任务.

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

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

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

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

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

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

如何终止线程池?

终止线程池主要有两种方式:

shutdown:"温柔"的关闭线程池,不接受新任务,但是在关闭前将之前提交的任务处理完毕

shutdownNow:"粗暴"的关闭线程池,也就是直接关闭线程池,通过Thread.interrupt()方法终止所有线程,不会等待之前提交的任务执行完毕,但是会返回队列中未处理的任务.

Executors提供了哪些创建线程池的方法?

newFixedThreadPool:固定线程数的线程池.corePoolsize=maximumPoolSize,keepAliveTime为0,工作队列使用无界的LinkedBlockingQueue.适用于为了满足资源管理的需求,而需要限制当前线程数量的场景,适合与负载比较重的服务器.

newSingleThreadExecutor:只有一个线程的线程池.corePoolSize=maximumPoolSize=1,keepAliveTime为0,工作队列使用无界的LinkedBlockingQueue.适用于需要保证顺序的执行各个任务的场景.

newCachedThreadPool:按需要创建新线程的线程池.核心线程数为0,最大线程数为integer.MAX_VALUE,keepAliveTime为60秒,工作队列使用同步移交SynchronousQueue.该线程池可以无限扩展,当需求增加时,可以添加新的线程,而当需求降低时会自动回收空闲线程.适用于执行很多的短期异步任务,或是负载较轻的服务器0

newScheduledThreadPool:创建一个以延迟或定时的方式来执行任务的线程池,工作队列为DelayedWorkQueue.适用于需要多个后台线程执行周期任务

newWorkStealingPool:JDK1.8新增,用于创建一个可以窃取的线程池,底层使用ForkJoinPool实现

线程池里的ctl是如何设计的?

ctl是一个打包两个概念的字段的原子整数.

1.workerCount:指示线程的有效数量

2.runState:指示线程的运行状态,有RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED 等状态。

int类型有32位,其中ctl的低29位用于表示workerCount,高3位用来表示runState.如下图

    RUNNING            -- 对应的高3位值是111。
    SHUTDOWN           -- 对应的高3位值是000。
    STOP               -- 对应的高3位值是001。
    TIDYING            -- 对应的高3位值是010。
    TERMINATED         -- 对应的高3位值是011。

ctl这样设计的好处是什么?

 这么设计的主要好处是将对 runState 和 workerCount 的操作封装成了一个原子操作,

runState 和 workerCount 是线程池正常运转中的2个最重要属性,线程池在某一时刻该做什么操作,取决于这2个属性的值。

因此无论是查询还是修改,我们必须保证对这2个属性的操作是属于“同一时刻”的,也就是原子操作,否则就会出现错乱的情况。如果我们使用2个变量来分别存储,要保证原子性则需要额外进行加锁操作,这显然会带来额外的开销,而将这2个变量封装成1个 AtomicInteger 则不会带来额外的加锁开销,而且只需使用简单的位操作就能分别得到 runState 和 workerCount。

线程池的大小配置多少合适?

首先我们需要区分任务是计算密集型还是I/O密集型

计算密集型:设置 线程数 = CPU数 + 1,通常能实现最优的利用率

对于I/O密集型 常见说法是 线程数 = CPU数 * 2 ,(个人觉得不是最优的)

在我们日常的开发中,我们的任务几乎是离不开I/O的,常见的网络I/O(RPC调用)、磁盘I/O(数据库操作),并且I/O的等待时间通常会占整个任务处理时间的很大一部分,在这种情况下,开启更多的线程可以让 CPU 得到更充分的使用,一个较合理的计算公式如下:

线程数 = CPU数 * CPU利用率 * (任务等待时间 / 任务计算时间 + 1)

例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个。

当然,具体我们还要结合实际的使用场景来考虑。如果要求比较精确,可以通过压测来获取一个合理的值
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值