前言
在最近的面试中,线程池是一个经常会被问到的点,所以今天来把线程池相关的知识详细的整理下。
什么是线程池?
线程池其实就是一个管理线程的池子,它可以帮我们创建、维护、销毁线程,类似于数据库连接池,在我们有任务需要执行时,我们不需要去手动的创建一个线程来执行这个任务,而是将任务交给线程池,由线程池内维护的线程来对任务进行处理。
为什么要用线程池?
- 线程的创建(需要开辟栈空间等操作)、销毁都需要浪费很多cPU资源,如果我们每个任务都重新创建线程,就会浪费大量资源,而引入线程池之后,我们就可以让线程复用,即一个线程完成一个任务后,可以继续去完成其它任务,就可以节省CPU的开销(可以参考数据库连接池)。
- 引入线程池让我们在执行任务时不必再关心线程创建、销毁的过程,让代码更加简介且便于维护。
线程池的状态
我们可以通过ThreadPoolExecutor来创建一个线程池,其中会用一个原子整数来表示线程池的状态和当前存活的线程数量(高3位表示线程池状态,低29位表示当前存活线程数量)。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
为什么要将两个信息存放到一个原子参数中?这是为了能让修改这两个信息只用一次原子操作就可以完成,保证了线程的安全性。
线程池分为以下状态:
线程池状态相关操作
- shutdown():线程池进入SHUTDOWN状态。
- shutdownNow():线程池进入STOP状态。
- tryTerminated():尝试终结线程池。
- isShutdown():只要线程池不是RUNNING状态就返回true。
- isTerminted():线程池是否处于TERMINATED状态。
- awaitTermination(long timeout, TimeUnit unit):判断在?时间后,线程池是否处于TERMINATED状态。
线程池的七大核心参数
- int corePoolSize:核心线程数。
- int maximumPoolSize:最大线程数。
- long keepAliveTime:紧急线程最大空闲时间。
- TimeUnit unit:紧急线程最大空闲时间的时间单位。
- BlockingQueue<Runnable> workQueue:阻塞队列。
- ThreadFactory threadFactory:线程工厂,为线程池内的线程命名,使命名统一规范。
- RejectedExecutionHandler handler:拒绝策略。
核心线程和紧急线程
在线程池的七大核心参数里,有核心线程数和最大线程数,他们两个的差值(最大线程数-核心线程数)就是紧急线程数的数量。它们的区别主要是:
- 核心线程:在有新任务且无空闲核心线程时被创建(未超过核心线程数时),创建后除非线程池关闭或线程出现异常,否则不会被销毁。
- 紧急线程:在有新任务且阻塞队列已满时被创建(未超过最大线程数时),创建后会存活一段时间,若超过最大空闲时间后仍没有新的任务来被它执行,该线程就会被销毁。
阻塞队列
当线程池内创建的核心线程数达到上线时,若此时再来新的任务,就会将任务放到阻塞队列里,等到有线程执行完任务后,再从堵塞队列里拿新的任务。堵塞队列主要有以下几种类型:
- ArrayBlockingQueue:基于数组实现的有界队列。
- LinkedBlockingQueue:基于链表实现的无界队列。
- DelayQueue:一个任务定时周期的延迟执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队列的先后排序。
- PriorityBlockingQueue:是具有优先级的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
拒绝策略
当阻塞队列已满且线程池内的线程数到达最大线程数时(均不空闲),这个时候再来新的任务就会触发拒绝策略。JDK主要提供了以下4中拒绝策略。
- AbortPolicy:让调用者抛出RejectExcutionException异常,这是默认策略。
- CallerRunsPolicy:让调用者运行任务。
- DiscardPolicy:放弃本次任务。
- DiscardOldestPloicy:放弃队列中最早的任务,本任务取而代之。
线程池的工作方式
- 当线程池刚被创建时,此时线程池内没有被创建的线程。
- 当任务到达时,若此时线程池内的线程数未达到核心线程数上限,且目前线程均处于工作状态,那么这时候线程池会为该任务创建一个新的核心线程来执行。等到该线程执行完该任务且没有新的任务时,这个线程不会被销毁。
- 当线程池内的核心线程数已到达上限(均在工作),且阻塞队列未满时,这时候新来的任务就会被放入堵塞队列里,等到有核心线程执行完任务后,就会到堵塞队列里拿新的任务来执行。
- 当线程池内的核心线程数已到达上限(均在工作),且阻塞队列已满时,此时若线程池内线程数未到达最大线程数,这时候线程池会创建一个紧急线程来处理该任务。当紧急线程执行完该任务后,会等待我们设置的最长空闲等待时间,若在该时间内有新的任务到来(依然满足堵塞队列已满的条件),这时候紧急线程就会去处理这个任务,若超过最大空闲等待时间仍没有新的任务,这个紧急线程就会被销毁掉。
- 当线程当线程池内的最大线程数已到达上限(均在工作),且阻塞队列已满时,这时候再来新的任务,就会触发拒绝策略。
线程池的常用方法
- execute():执行任务,无返回值。
- submit():执行任务,有返回值,通过Future接收。
- invokeAll():可执行任务列表中的所有任务,可带超时时间,超过该时间没有执行完的任务不再执行。
- invokeAll():提交任务列表中的所有任务,只要有一个任务执行成功就返回结果,然后停止其它所有任务。
常用线程池
JDK为了方便我们使用,为我们提供了一些默认的线程池(参数已经设置好),这里主要介绍以下四种:
NewFixThreadPool
固定大小的线程池,主要特征有:
- 核心线程数=最大线程数,无需设置最大空闲时间
- 无界阻塞队列
- 适应于任务量已知,相对耗时的任务
NewCachedThreadPool
带缓冲线程池,主要特征有:
- 核心线程数为0,最大线程数为Integer.MAX_VALUE,最大空闲时间为60s,即全部都是紧急线程且空闲60s后回收,紧急线程可以无限创建
- 队列采用了SynchronousQueue,它没有容量,必须有线程.来取时才能将任务放入,否则一直阻塞。
- 适合任务比较密集但执行时间较短
NewSingleThreadExecutor
单线程线程池,主要特征邮件:
- 核心线程数和最大线程数均为1,执行完毕线程不会释放
- 采用无界队列
- 与自己创建单线程相比,如果任务失败导致线程终止,线程池会重新拆改那就一个线程
- 与newFixedThreadPool(1)相比,其线程个数始终为1,不能修改,其采用了一个装饰器模式,对外暴露了ExecutorService接口,而没有暴露ThreadPoolExecutor对象(强转后修改线程数)。
- 适应于多个任务排队执行
newScheduledThreadPool
周期执行的线程池,主要特征有:
- 最大线程数为Integer.MAX_VALUE
- 阻塞队列是DelayedWorkQueue
- keepAliveTime为0
- scheduleAtFixedRate() :按某种速率周期执行
- scheduleWithFixedDelay():在某个延迟后执行
- 适应于周期性执行某个任务的场景。
如何选择合适的线程数?
在我们真正需要应用线程池时,该如何确定线程池的线程数是一个问题,如果线程数设置过大,可以能造成资源浪费,如果线程数设置过小,又会使线程不够用,造成堵塞队列过长或者任务大量被拒绝。在实际使用时,一般我们会采用以下规律:
- IO密集型:应该考虑更多的线程数和较小的堵塞队列,一般设置为2*CPU核心数。
这是因为线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。所以对于IO任务可以多分配写线程。
- CPU密集型:线程数量不宜过多,一般设置为CPU核心数+1,但会需要较长的队列做缓冲。
这是因为线程一定调度到某个 CPU 进行执行,如果任务本身是 CPU 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量
- 混合型:可以将任务进行分类,分成两个线程池来处理。