线程池
1.常见线程池
1.1 newSingleThreadExecutor(单线程化)
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
1.2 newFixedThreadPool(定长)
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
1.3 newCachedThreadPool(可缓存)
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
1.4 newScheduledThreadPool(定时)
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
核心概念:这四个线程池的本质都是ThreadPoolExecutor对象(自己看源码) 不同点在于: 1)FixedThreadPool:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。 2)ScheduledThreadPool:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。 3)CachedThreadPool:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。 4)SingleThreadExecutor:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列
2.什么是线程池
什么是线程池
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象。它可以容纳多个线程,其中的线程可以反复利用,省去了频繁创建线程对象的操作
线程池的优点:降低资源消耗、提高响应速度、方便管理;线程可以复用、可以控制最大并发数、可以管理线程
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务; 2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务; 3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等; 4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
线程池参数
corePoolSize:核心线程数量,会一直存在,除非allowCoreThreadTimeOut设置为true,核心线程也会超时回收。 maximumPoolSize:线程池允许的最大线程池数量 当活跃线程数达到该数值后,后续的新任务将会阻塞。 keepAliveTime:线程数量超过corePoolSize,空闲线程的最大超时时间 unit:超时时间的单位 workQueue:工作队列,保存未执行的Runnable 任务 threadFactory:创建线程的工厂类 handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。
1)corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。 2)maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。 3)keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。 4)unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。 5)workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。 6)threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。 7)handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
四种拒绝策略
new ThreadPoolExecutor.AbortPolicy(默认)
线程池默认拒绝策略,如果元素添加到线程池失败,丢弃任务会抛出RejectedExecutionException异常 new ThreadPoolExecutor.CallerRunsPolicy() 如果添加失败,那么主线程会自己调用执行器中的execute方法来执行任务 ,由调用线程处理该任务。 new ThreadPoolExecutor.DiscardPolicy() 如果添加失败,则放弃,不会抛出异常 new ThreadPoolExecutor.DiscardOldestPolicy() 如果添加到线程池失败,会将队列中最早添加的元素移除,再尝试添加,如果失败则按该策略不断重试
创建方式
Executors.newSingleThreadExecutor(); 创建一个单一的线程池 Executors.newFixedThreadPool(int nThreads); 创建一个固定大小的线程池,参数填线程池大小 Executors.newCachedThreadPool(); 创建一个可伸缩的线程池,遇强则强,遇弱则弱
3 为什么不建议使用 Executors静态工厂构建线程池
:Executors返回的线程池对象的弊端如下:
1:FixedThreadPool 和 SingleThreadPool: 允许的请求队列(底层实现是LinkedBlockingQueue)长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM 2:CachedThreadPool 和 ScheduledThreadPool 允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
创建线程池的正确姿势
private static ExecutorService executor = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));
避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。
ThreadPoolExecutor 手动的方式来创建线程池,因为这种方式可以通过参数来控制最大任务数和拒绝策略,让线程池的执行更加透明和可控,并且可以规避资源耗尽的风险。
ThreadPoolExecutor 手动创建线程池的方式,创建一个最大线程数为 2,最多可存储 2 个任务的线程池,并且设置线程池的拒绝策略为忽略新任务,这样就能保证线程池的运行内存大小不会超过 10M 了,里面所有的参数我们都可以根据需求来自己指定.
其中使用 Executors 自动创建线程的方式,因为线程个数或者任务个数不可控,可能会导致内存溢出的风险,所以在创建线程池时,建议用ThreadPoolExecutor 手动的方式来创建线程池。
4、线程池的工作流程?
这个问题回答的时候,最好用讲故事的方式进行。 假如核心线程数是5,最大线程数是10,阻塞队列也是10 1)有新任务来的时候,将先使用核心线程执行; 2)当任务数达到5个的时候,第6个任务开始排队; 3)当任务数达到15个的时候,第16个任务将开启新的线程执行,也就是第6个线程 4)当任务数达到20个的时候,线程池满了,如果有第21个任务,将执行拒绝策略
线程只能在任务到达时才启动吗?
默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是我们可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。
5.线程池优化了解吗(40%可能性被问到)?
这个问题和第7个问题很类似,可以参考回答。其他答案: 1)用ThreadPoolExecutor自定义线程池,看线程是的用途,如果任务量不大,可以用无界队列,如果任务量非常大,要用有界队列,防止OOM 2)如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务 3)最大线程数一般设为2N+1最好,N是CPU核数 4)核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数 5)如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果。
6.如何合理设置线程池的核心线程数?
线程数量的计算公式一般都是 线程数=Ncpu(1+w/e).其中W代表的是阻塞耗时,e代表的是计算耗时。 1)IO密集型:如果存在IO,那么W/e肯定大于1,但是需要考虑系统内存上限(没开启一个线程都需要内存空间),这个需要服务器测试到底多少个线程比较合适(CPU占比,线程数、总耗时、内存耗时)。保守取值为1,及线程数=2Ncpu+1, 2)计算密集型:假设没有等待时间,则W=0,W/C=0,线程数= Ncpu+1. 其中多出来的一个是为了防止线程偶发的缺页中断。 服务性能I0优化有一个估算公式: 最佳线程数目=((线程等待时间+线程CPU时间)/线程CPU时间)X CPU数量 比如平均每个线程CPU运行时间为0.5s,而线程等待时间为1.5s(比如IO),CPU个数为8.则根据以上公式可以估算((1.5+0.5)/0.5)X 8=32 公式进一步转化: 最佳线程数目 = (线程等待时间/线程CPU时间+1)X 线程数
6.1在我们实际使用中,线程池的大小配置多少合适?
要想合理的配置线程池大小,首先我们需要区分任务是计算密集型还是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个。
当然,具体我们还要结合实际的使用场景来考虑。如果要求比较精确,可以通过压测来获取一个合理的值。
7.线程池里有个 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。