1.对象的初始化顺序(类)
2.线程池的复用原理
任务队列:当没有直接的任务要执行时,核心线程会等待任务队列中的新任务。这是通过workQueue.take()
操作实现的,该操作会导致线程阻塞,直到有可用的任务。
3.核心线程数和非核心线程数的区别
核心线程与非核心线程区别
核心线程只是一个叫法, 核心线程与非核心线程的区别是:
创建核心线程时会携带一个任务, 而非核心线程没有, 如果核心线程执行完第一个任务, 线程池内线程无区别
线程池是期望达到 corePoolSize 的并发状态, 不关心最先添加到线程池的核心线程是否会被销毁
对最后一句话的解释:
-
期望达到
corePoolSize
的并发状态:corePoolSize
是线程池配置中的一个关键参数,代表核心线程数。线程池初始化时并不会立即创建这么多线程,但当任务持续提交给线程池时,线程池会尽量快速创建线程,直到达到corePoolSize
指定的数量。- 这些核心线程是线程池尝试始终保持活动的,因为它们通常用于处理最常见的工作负载。
-
不关心最先添加到线程池的核心线程是否会被销毁:
- 通常情况下,核心线程是不会被销毁的,即使它们处于空闲状态。这一特性确保了当新的任务到来时,线程池可以迅速响应,因为它总有一些线程在待命。
- 但在
ThreadPoolExecutor
中,有一个allowCoreThreadTimeOut
参数。当它设置为true
时,核心线程在空闲超过keepAliveTime
指定的时间后,会被销毁。这意味着,线程池可能并不关心最早创建的核心线程是否被销毁,只要总体线程数可以维持在corePoolSize
附近。
这样的设计提供了一种平衡。通过维持一个核心线程集,线程池可以确保对任务的快速响应。同时,通过允许核心线程超时并最终被销毁,线程池也可以在低负载时节省资源。
简而言之,这句话描述了线程池的目标:它希望尽可能快地达到一个优化的并发状态(即核心线程数),同时也能够在空闲时适当地释放资源。
4.线程池的工作线程(worker)和task的关系:
在线程池中,任务和线程的关系非常核心。理解它们之间的关系有助于我们更好地理解线程池是如何工作和为什么使用线程池可以优化性能的。以下是任务和线程之间在线程池中的基本关系:
-
任务 (Tasks):
- 任务通常是一些单独的工作单元或操作,这些操作需要被执行。
- 在 Java 中,任务通常是实现了
Runnable
或Callable
接口的对象。 - 当我们有某个操作需要异步或并行执行时,我们会将其包装为一个任务并提交给线程池。
-
线程 (Threads):
- 线程是操作系统中的基本执行单位。
- 在 Java 的线程池中,线程主要是来执行提交给线程池的任务。
- 一个线程在任何给定的时间点只能执行一个任务,但在其生命周期中,它可以执行多个任务。
-
任务与线程的关系:
- 当一个任务被提交到线程池时,线程池会根据其当前的状态和配置来决定如何处理这个任务。
- 如果有空闲线程可用,这个任务可能会被立即分配给一个空闲线程去执行。
- 如果所有线程都在忙,并且当前线程数还没达到最大线程数,线程池可能会创建一个新的线程来执行这个任务。
- 如果所有线程都在忙,且线程数已经达到最大值,那么这个任务会被放入
workQueue
中等待执行。 - 当一个线程完成了其当前的任务后,它会从
workQueue
中取出下一个任务并开始执行。 - 如果
workQueue
也满了,线程池会采用其拒绝策略(通常是抛出一个异常)来处理新提交的任务。
总之,线程池中的线程和任务的关系是一种生产者消费者关系。我们提交的任务是"生产者"产生的产品,而线程池中的线程是"消费者",它们消费和处理这些任务。线程池的目标是确保线程始终保持忙碌(但不过载),以便最大限度地提高效率和性能。
5.线程池中的锁
线程池中有两把锁:mainlock(可重入锁),worker(本身继承的AQS)
1.mainlock:
由于worker是由hashset集合,用mainlock维护多线程对hashset集合的并发操作。
2.work中的锁(不可重入)
用于维护work工作线程的中断状态(保证正在执行任务的线程是不应该被中断的)
work继承AQS,state初始为-1,空闲为0(cas(0,1)就是要把空闲线程中断),运行为1,
为什么创建work的时候不直接state=0,因为线程池中work线程的创建和运行是分为两步的
为什么不初始为
6.线程池的提交流程
7.线程池回收线程
8.场景题:一瞬间提交140个请求,核心线程数为40,最大线程数为100,请求最大延迟为5ms,平均rt为3ms,如何设计线程池参数降低处理延迟?
首先,理解场景需求:
- 140个请求瞬间提交:这意味着在一个很短的时间内,你将有大量请求需要处理。
- 核心线程数为40:这意味着有40个线程始终处于活动状态(如果有请求要处理)。
- 最大线程数为100:这意味着当核心线程已满,并且等待队列也已满时,线程池还可以再创建最多60个线程来处理请求。
- 请求最大延迟为5ms:这是一个很高的性能要求,意味着每个请求都要在5ms内完成。
- 平均rt为3ms:这意味着每个请求平均需要3ms来处理。
考虑到这些需求,提供以下建议:
-
核心线程数:鉴于瞬间有140个请求,你可能需要增加核心线程数以立即处理这些请求。将核心线程数提高到70或80可能更有意义。
-
最大线程数:当前设置的最大线程数为100似乎已经很合理,因为这允许线程池在高负载时创建更多的线程来处理请求。
-
等待队列:你需要设置一个等待队列,以便在所有核心线程都被使用时,其他的请求可以在队列中等待。考虑到平均rt为3ms,这个队列可能不需要太大。可能的话,使用一个大小为70或80的队列。
-
线程存活时间:如果非核心线程在空闲时可以被回收,你可以设置一个较短的线程存活时间(如10-30秒),这样线程池可以在低负载时释放资源。
-
拒绝策略:这是线程池在达到最大线程数并且等待队列已满时的行为。考虑到这个场景的性能要求,
AbortPolicy
(直接抛出异常)可能不是最佳选择。CallerRunsPolicy
(让调用线程处理任务)可能是一个更好的选择,因为这至少允许请求被处理,而不是直接拒绝。 -
任务处理策略:考虑到这些请求都是瞬时的,并且你希望他们在5ms内完成,你需要确保线程池中的线程正在优先处理新进来的任务。这可能意味着你需要使用一个LIFO(后进先出)的策略,而不是默认的FIFO(先进先出)策略。
-
资源监视与自动调整:考虑使用JMX或其他监视工具,以便你可以实时监视线程池的状态并在需要时进行调整。
要确保线程池中的任务采用后进先出(LIFO)的执行策略,可以使用一个基于Stack
的数据结构,或者具有LIFO特性的Deque
(双端队列)。
在Java的ThreadPoolExecutor
中,任务默认是存储在一个基于FIFO策略的BlockingQueue
中的。要实现LIFO策略,可以使用LinkedBlockingDeque
,但记住默认情况下LinkedBlockingDeque
是按FIFO方式操作的。为了使其以LIFO方式工作,需要使用其push
方法添加任务,并使用pop
方法取出任务。
9.awaitTermination()
awaitTermination()
是线程池ThreadPoolExecutor
的一个方法。当你调用它时,它会使当前的线程等待直到线程池中的所有任务都完成并且所有工作线程都结束,或者直到你指定的时间到期。
换句话说,当你想关闭线程池并且希望等待它完全关闭时,你会首先调用shutdown()
(或shutdownNow()
),然后调用awaitTermination()
。
例如:
- 你调用
shutdown()
来告诉线程池不再接受新的任务。 - 然后,你调用
awaitTermination(10, TimeUnit.SECONDS)
。这会使当前线程等待10秒,看线程池是否在这段时间内关闭。 - 如果10秒后,线程池中的所有任务都完成并且所有线程都结束,那么
awaitTermination()
返回true
。否则,如果还有任务正在运行或10秒时间到期,它返回false
。
简而言之,awaitTermination()
是一种方法,允许你等待线程池完全关闭,或者等待一段指定的时间。
10.线程池不能与threadlocal混用
线程池和 ThreadLocal
共用,可能会导致线程从ThreadLocal
获取到的是旧值/脏数据。这是因为线程池会复用线程对象,与线程对象绑定的类的静态属性 ThreadLocal
变量也会被重用,这就导致一个线程可能获取到其他线程的ThreadLocal
值