线程池知识点
1、什么是线程池?
线程池是一种利用池化技术思想来实现的线程管理技术,将线程对象放入线程池达到线程对象的重用。线程池用来降低频繁创建和销毁线程所带来的资源消耗。
优点:
1)、降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2)、提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
3)、提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
使用场景:
服务器接受到大量请求时,使用线程池技术是非常合适的,它可以大大减少线程的创建和销毁次数,提高服务器的工作效率。
2、如何创建线程池?
Java
中的线程池是通过Executor
框架实现的,该框架中用到了Executor
、Executors
、ExecutorService
、 ThreadPoolExecutor
、Callable
、Future
、FutureTask
这几个类。
方式一:通过构造方法实现:ThreadPoolExecutor
。
方式二:通过Executor
框架的工具类Executors
来实现,可以创建现有的几种类型的线程池。
Executors
创建线程池对象存在如下弊端:
1)、SingleThreadExecutor和FixedThreadPool
:允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM
。
2)、CachedThreadPool
:允许的创建线程数量为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致OOM
。
建议:
创建线程池最好不要使用Executors
去创建,而是通过ThreadPoolExecutor
的方式去创建。
Executors
创建线程池其实也是调用ThreadPoolExecutor
实现的。
3、创建线程池的几个核心构造参数?
Java
中的线程池的创建非常灵活,可以通过配置不同的参数,创建出行为不同的线程池。
1)、corePoolSize
:线程池的核心线程数,线程池中常驻线程的最大数量。默认线程就算是空闲的,也不会被销毁。设置了allowCoreThreadTimeOut
之后如果线程空闲并且超过设置的时间空闲线程会被销毁。
2)、maxPoolSize
:线程池允许的最大线程数,包括核心线程和非核心线程。
3)、keepAliveTime
:超过核心线程数时闲置线程(非核心线程)的存活时间。
4)、unit
:keepAliveTime
值的时间单位。
5)、workQueue
:任务执行前存放任务的阻塞队列,保存由execute
方法提交的Runnable
任务。
6)、threadFactory
:创建线程的工厂,如果未指定则使用默认的线程工厂。
7)、handler
:任务无法执行时的处理策略(拒绝策略),指定了当任务队列已满,并且没有可用线程执行任务时对新添加的任务的处理策略。
4、线程池执行流程?
线程池默认初始化后不启动 Worker
,等待有请求时才启动。
每当我们调用execute()
方法添加一个任务时,线程池会做如下判断:
- 如果正在运行的线程数量没有达到核心线程数(
corePoolSize
),那么马上创建线程运行这个任务。 - 如果正在运行的线程数量达到核心线程数(
corePoolSize
),那么将这个任务放入队列。 - 如果这时候队列满了,而且正在运行的线程数量没有达到最大线程数(
maxPoolSize
),那么还是要创建非核心线程立刻运行这个任务。 - 如果队列满了,而且正在运行的线程数量达到最大线程数(
maxPoolSize
),那么线程池根据拒绝策略来处理。
当一个线程完成任务时,它会从队列中取下一个任务来执行,当一个线程无事可做,超过一定的时间(keepAliveTime
)时,线程池会判断,如果当前运行的线程数大于corePoolSize
,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到corePoolSize
的大小。
是否需要增加线程的判断顺序是:corePoolSize
、workQueue
、maxPoolSize
。
5、线程池五种状态?
1)、running
:指的是线程池的初始化状态(被创建),接受新任务并处理排队任务。
2)、shutdown
:指的是线程池处于待关闭状态,不接受新任务,但处理排队任务。
3)、stop
:指的是线程池立即关闭,不接受新任务,也不处理排队任务,并中断正在进行的任务。
4)、tidying
:指的是线程池自主整理状态,所有任务都已终止,线程会转换到tidying
状态,并将运行terminate()
钩子方法。
5)、terminated
:指的是线程池彻底终止状态,terminate()
运行完成。
状态之间的变化过程:
running
状态—>调用shutdown()
方法—>shutdown
状态—>队列为空,并且线程池种执行的任务也为空—>tidying
running|shutdown
状态—>调用shutdownNow()
方法—>stop
状态—>线程池种中执行的任务为空—>tidying
tidying
状态—>terminate()
执行完—>terminated
线程池中的terminated()
方法是空实现,可以重写该方法进行相应的处理。
6、线程池的拒绝策略?
线程池中的线程已经用完了,无法继续为新任务服务,同时等待队列也已经排满了,再也塞不下新任务了,这时候我们就需要拒绝策略机制合理的处理这个问题。拒绝策略就是当队列满时,线程如何去处理新来的任务。
1)、AbortPolicy
:默认策略,指的是丢弃任务,抛出异常。
2)、CallerRunsPolicy
:由提交任务的当前线程处理该任务,也就是在主线程中执行任务。
3)、DiscardOldestPolicy
:丢弃最老的还没执行的任务。
4)、DiscardPolicy
:直接丢弃任务,也不抛异常。
7、常用的工作队列?
1、ArrayBlockingQueue
:使用数组实现的有界阻塞队列,初始化时指定的容量,就是队列最大的容量,必须要给它指定一个队列的大小,特性先进先出。
2、LinkedBlockingQueue
:使用链表实现的阻塞队列,可以设置其容量,默认为Interger.MAX_VALUE
,被称为无界队列,特性先进先出。
3、PriorityBlockingQueue
:使用平衡二叉堆,实现的具有优先级的无界阻塞队列。/praɪˈɒrəti/
4、DelayQueue
:无界阻塞延迟队列,队列中每个元素均有过期时间,当从队列获取元素时,只有过期元素才会出队列,队列头元素是最快要过期的元素。
5、SynchronousQueue
:一个不存储元素的阻塞队列,每个插入操作,必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。/ˈsɪŋkrənəs/
6、LinkedTransferQueue
:由链表结构组成的无界阻塞队列。
7、LinkedBlockingDeque
:由链表结构组成的双向阻塞队列。
8、几种常用的线程池?
Java
里面线程池的顶级接口是Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具,真正的线程池接口是ExecutorService(ExecutorService extends Executor)
。
Executors
类主要用于提供线程池相关的操作,它提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService
接口。
1)、newSingleThreadExecutor
:只有一个核心线程的线程池,使用无界队列LinkedBlockingQueue
,用于多个任务需要按顺序执行,也就是串行执行,如果线程异常会创建一个新的线程。
2)、newFixedThreadPool
:固定大小的线程池,只有核心线程(corePoolSize
等于maximumPoolSize
),使用无界阻塞队列LinkedBlockingQueue
,用于需要控制并发线程数和处理CPU密集型的任务和执行长期的任务。如果线程异常会创建一个新的线程。当线程处于空闲状态时,他们并不会被回收,除非线程池被关闭。
3)、newCachedThreadPool
:核心线程为0,非核心线程无限制(Integer.MAX_VALUE
),无界线程池,使用SynchronousQueue
阻塞队列,一个缓冲区为1的阻塞队列,用于耗时较短的任务和任务处理速度>任务提交速度的任务,这样才能保证不会不断创建新的线程,避免资源耗尽。如果线程数超过任务数(空闲60s),会回收线程,如果超过,会增加线程。
4)、newScheduledThreadPool
:非核心线程为0,核心线程池固定,大小无限的线程池(Integer.MAX_VALUE
),工作队列使用DelayQueue
,适用于定时以及周期性执行任务。如果闲置,核心线程池会在DEFAULT_KEEPALIVEMILLIS
时间内回收。
两种方式提交任务:
scheduleAtFixedRate
:按照固定速率周期执行。scheduleWithFixedDelay
:上个任务延迟固定时间后执行。
5)、newWorkStealingPool
:任务窃取线程池,不保证执行顺序,适合任务耗时差异较大。主线程结束,即使线程池有任务也会立即停止。
6)、newSingleThreadScheduledExecutor
:单线程可执行周期性任务的线程池,定期或延时执行任务,定时心跳检测任务。
9、使用无界队列的线程池会导致内存飙升吗?
会的,newFixedThreadPool
使用了无界的阻塞队列LinkedBlockingQueue
,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升,最终导致OOM
。
10、如何在Java线程池中提交线程?
线程池最常用的提交任务的方法有两种:
1、execute()
:ExecutorService.execute()
方法接收一个Runable
实例,它用来执行一个任务,没有返回值,不能判断是否执行成功。ExecutorService.execute()
。
2、submit()
:ExecutorService.submit()
方法返回的是Future
对象,可以用isDone()
来查询Future
是否已经完成,当任务完成时,它具有一个结果,可以调用get()
来获取结果。也可以不用isDone()
进行检查就直接调用get()
,在这种情况下,get()
将阻塞,直至结果准备就绪。submit
提交线程可以吃掉线程中产生的异常,达到线程复用。当执行get()
获取结果时异常才会抛出,原因是通过submit
提交的线程,当发生异常时,会将异常保存,待future.get()
时才会抛出。
11、线程池中的核心线程如何设置呢?
CPU
密集型(加密、计算Hash等):核心线程数=CPU个数+1。
IO
密集型(读写数据库、文件、网络读写等):核心线程数 = CPU个数*2。
这个只是个理论值,具体设置大小,建议在本地、测试、准生产环境下调试出相对最优参数大小。
12、线程池例子?
一个线程池core 7,max 20,queue 50,100并发进来怎么分配的?
先有7个能直接得到执行,接下来把50个进入队列排队等候, 在多开13个继续执行,现在70个被安排上了,剩下30个默认执行饱和策略。
13、线程池出现异常会发生什么?
线程出现异常,线程会退出,并重新创建新的线程执行队列里任务,不能复用线程。
当业务代码的异常捕获了,线程就可以复用。
使用ThreadFactory
的UncaughtExceptionHandler
保证线程的所有异常都能捕获(包括业务的异常),兜底的。
如果提交方式用execute
,不能复用线程。
setUncaughtExceptionHandler+submit
:可以吃掉异常并复用线程(是吃掉,不报错)。
setUncaughtExceptionHandler+submit+future.get()
:可以获取到异常并复用线程。
最佳实践:
提交线程的业务异常用try catch
处理,保证线程不会异常退出。
业务之外的异常我们不可预见的,创建线程池设置ThreadFactory
的UncaughtExceptionHandler
可以对未捕获的异常做保底处理,通过submit
提交任务,可以吃掉异常并复用线程,想要捕获异常这时用future.get()
。
14、Java中的线程池是如何实现的?线程池原理?线程复用?
在Java
中所谓的线程池中的线程,其实是被抽象为了一个静态内部类Worker
,它基于AQS
实现,存放在线程池的HashSet<Worker>workers
成员变量中。而需要执行的任务则存放在成员变量BlockingQueue<Runnable> workQueue
中。这样整个线程池实现的基本思想就是:从workQueue
中不断取出需要执行的任务,放在Workers
中进行处理。如果从workQueue
中获取到的任务不为null
,这样则能够复用线程执行任务。
线程复用:线程池中维护了一个Worker
的内部类,其中Worker
也实现了Runnable
接口,重写了run()
方法,在调用这个run()
时候,会采用类似于死循环的while
方式重复使用这个worker
去获取任务并执行我们传入execute(Runnable task)
方法的参数task
(task存放在阻塞队列里)。
15、Java中的线程池的组成部分?
1、线程池管理器:用于创建并管理线程池。
2、工作线程:线程池中的线程。
3、任务接口:每个任务必须实现的接口,用于工作线程调度其运行。
4、任务队列:用于存放待处理的任务,提供一种缓冲机制。
16、核心工作线程是否会被回收?
线程池中有个allowCoreThreadTimeOut
字段能够描述是否回收核心工作线程,线程池默认是false
表示不回收核心线程,我们可以使用allowCoreThreadTimeOut(true)
方法来设置线程池回收核心线程。
17、停止线程池的正确方法?
1)、shutdown
:一种安全关闭线程池的方法,调用这个方法后线程池会根据拒绝策略来拒绝新提交的任务,然后线程池会把正在执行的任务和在队列中等待任务都执行完后才会彻底关闭。
2)、shutdownNow
:调用了这个方法后线程池就会立即关闭,会给所有线程池中的线程发送interrupt
中断信号,来尝试中断这些任务的执行,不过需要线程自身具有响应中断信号的能力,同时可以发现这个方法有个Runnable
集合返回,这集合就是那些任务队列中正在等待被执行的任务集合,返回之后,开发者就可以根据这任务集合做一些补救的操作,比如先记录后面再重试。
3)、isShutdown
:一个判断线程池是否关闭的方法,不过这里的关闭不是指真的关闭,而是指是否开始了关闭流程。
4)、isTerminated
:只有线程池里所有的任务都被执行完,调用这个方法才会返回true
,表示线程池已关闭且内部的任务都已经执行完毕了。
5)、awaitTermination
:一个判断线程池状态的方法,可以发现有两个参数timeout
和unit
构成了一个时间参数,调用了这个方法后,当前线程会等待一段指定的时间,如果在这个时间段内,线程池已经关闭且内部的任务都已经执行完毕了,那就会返回true
,否则返回false
。
18、多线程中抛异常?
一个线程池中的线程异常了,那么线程池会怎么处理这个线程?
严格说是:一个线程池中的线程抛出了未经捕获的运行时异常,那么线程池会怎么处理这个线程?
先给大家看个案例:
sayHi
方法是会抛出运行时异常的。
- 当执行方式是
execute
方法时,在控制台会打印堆栈异常。 - 当执行方式是
submit
方法时,在控制台不会打印堆栈异常。
想要获取这个submit
方法提交时的异常信息得调用返回值future
的get
方法。
当一个线程池里面的线程异常后:
1、当执行方式是execute
时,可以看到堆栈异常的输出。
2、当执行方式是submit
时,堆栈异常没有输出。但是调用Future.get()
方法时,可以捕获到异常。
3、不会影响线程池里面其他线程的正常执行。
4、线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。
当执行方法是submit
的时候,如果子线程抛出未经捕获的运行时异常,将会被封装到Future
里面,那么如果子线程捕获了异常,该异常不会封装到Future
里。
现在是用submit
的方式往线程池里面提交任务,而执行的这个任务会抛出运行时异常。
对于抛出的这个异常,我们分为两种情况:
- 子线程中捕获了异常,则调用返回的
future
的get
方法,不会抛出异常。 - 子线程中没有捕获异常,则调用返回的
future
的get
方法,会抛出异常。
是怎么实现的呢:
FUTURE
:
一个任务的终态有四种:NORMAL
、EXCEPTIONAL
、CANCELLED
、INTERRUPTED
。
当FutureTask
的status
为NORMAL
时正常返回结果,当status
为EXCEPTIONAL
时抛出异常。
线程池
:
在java.util.concurrent.FutureTask#run
方法里面。
如果子线程捕获了异常,该异常不会被封装到Future
里面,是通过FutureTask
的run
方法里面的setException
和set
方法实现的。在这两个方法里面完成了FutureTask
里面的outcome
变量(根据任务的状态将结果返回)的设置,同时完成了从NEW
到 NORMAL
或者EXCEPTIONAL
状态的流转。
不论是用submit
还是execute
方法往线程池里面提交任务,如果由于线程池满了,会抛出拒绝异常。RejectedExecutionException
异常也是一个RuntimeException
。
终极答案
:
异常日志的打印和哪种方式提交任务没有关系,不论哪种,只要你没有捕获异常,则都会触发 dispatchUncaughtException
方法。由于线程不能抛出可以捕获异常,所以需要线程出现异常时jvm
调用dispatchUncaughtException
方法回调线程异常处理器处理。如果设置了该线程异常处理就是用设置的,没有就是用父线程组的,不然就使用全局线程处理,再不然就直接打印错误。
我们现在把情况分为三种:
第一种:submit
方法提交一个会抛出运行时异常的任务,捕不捕获异常都可以。无论如何都不会触发dispatchUncaughtException
方法。因为submit
方法提交,不论你捕获与否,源码里面都帮你捕获了。
第二种:execute
方法提交一个会抛出运行时异常的任务,不捕获异常。如果不捕获异常,会触发dispatchUncaughtException
方法,因为runWorker
方法的源码里面虽然捕获了异常,但是又抛出去了,而我们自己没有捕获,所以会触发dispatchUncaughtException
方法。
第三种:submit
或者execute
提交,让线程池饱和之后抛出拒绝异常,代码没有捕获异常。和第二种其实是一样的。没有捕获,就会触发。