转载面试必问的线程池你懂了吗

2次阅读

共计 4703 个字符,预计需要花费 12 分钟才能阅读完成。

前言

在上次和二狗的“HashMap 最强者”PK 后,二狗始终耿耿于怀,常常缠着我要复仇,甚至违心出卖本人的屁股???我破口大骂:“这个死基佬”,而后许可了他 …

于是“独身狗大厦 11 楼 11 室”又是一场血雨腥风。

注释

二狗:为什么要应用线程池?间接 new 个线程不是很难受?

如果咱们在办法中间接 new 一个线程来解决,当这个办法被调用频繁时就会创立很多线程,不仅会耗费系统资源,还会升高零碎的稳定性,一不小心 把零碎搞崩了 ,就能够 间接去财务那结帐 了。

如果咱们正当的应用线程池,则能够防止把零碎搞崩的困境。总得来说,应用线程池能够带来以下几个益处:

  1. 升高资源耗费。通过反复利用已创立的线程,升高线程创立和销毁造成的耗费。
  2. 进步响应速度。当工作达到时,工作能够不须要等到线程创立就能立刻执行。
  3. 减少线程的可管理型。线程是稀缺资源,应用线程池能够进行统一分配,调优和监控。

二狗:线程池的外围属性有哪些?

threadFactory(线程工厂):用于创立工作线程的工厂。

corePoolSize(外围线程数):当线程池运行的线程少于 corePoolSize 时,将创立一个新线程来解决申请,即便其余工作线程处于闲暇状态。

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

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

handler(回绝策略):往线程池增加工作时,将在上面两种状况触发回绝策略: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(启动全副外围线程)办法来提前启动外围线程。

二狗:外围线程怎么实现始终存活?

阻塞队列办法有四种模式,它们以不同的形式解决操作,如下表。

抛出异样

返回非凡值

始终阻塞

超时退出

插入

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() 办法终止所有线程,不会期待之前提交的工作执行结束。然而会返回队列中未解决的工作。

二狗:(粗犷?切实是太刺激了,不过我喜爱)那我再问问你,Executors 提供了哪些创立线程池的办法?

newFixedThreadPool:固定线程数的线程池。corePoolSize = maximumPoolSize,keepAliveTime 为 0,工作队列应用无界的 LinkedBlockingQueue。实用于为了满足资源管理的需要,而须要限度以后线程数量的场景,实用于负载比拟重的服务器。

newSingleThreadExecutor:只有一个线程的线程池。corePoolSize = maximumPoolSize = 1,keepAliveTime 为 0,工作队列应用无界的 LinkedBlockingQueue。实用于须要保障程序的执行各个工作的场景。

newCachedThreadPool:按须要创立新线程的线程池。外围线程数为 0,最大线程数为 Integer.MAX_VALUE,keepAliveTime 为 60 秒,工作队列应用同步移交 SynchronousQueue。该线程池能够有限扩大,当需要减少时,能够增加新的线程,而当需要升高时会主动回收闲暇线程。实用于执行很多的短期异步工作,或者是负载较轻的服务器。

newScheduledThreadPool:创立一个以提早或定时的形式来执行工作的线程池,工作队列为 DelayedWorkQueue。实用于须要多个后盾线程执行周期工作。

newWorkStealingPool:JDK 1.8 新增,用于创立一个能够窃取的线程池,底层应用 ForkJoinPool 实现。

二狗:线程池里有个 ctl,你晓得它是如何设计的吗?

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

1)workerCount:批示线程的无效数量;

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

int 类型有 32 位,其中 ctl 的低 29 为用于示意 workerCount,高 3 位用于示意 runState,如下图所示。

例如,当咱们的线程池运行状态为 RUNNING,工作线程个数为 3,则此时 ctl 的原码为:1010 0000 0000 0000 0000 0000 0000 0011

二狗:(这小子看来由偷偷筹备了,看来得拿出压箱底的题目了)ctl 为什么这么设计?有什么益处吗?

集体认为,ctl 这么设计的次要益处是将对 runState 和 workerCount 的操作封装成了一个原子操作。

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

因而无论是查问还是批改,咱们必须保障对这 2 个属性的操作是属于“同一时刻”的,也就是原子操作,否则就会呈现错乱的状况。如果咱们应用 2 个变量来别离存储,要保障原子性则须要额定进行加锁操作,这显然会带来额定的开销,而将这 2 个变量封装成 1 个 AtomicInteger 则不会带来额定的加锁开销,而且只需应用简略的位操作就能别离失去 runState 和 workerCount。

因为这个设计,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。

二狗:小伙子不错不错,那我最初问一个,在咱们理论应用中,线程池的大小配置多少适合?

要想正当的配置线程池大小,首先咱们须要辨别工作是计算密集型还是 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 个。

当然,具体咱们还要结合实际的应用场景来思考。

正文完
 0