线程池

227 阅读8分钟

前言

在最近的面试中,线程池是一个经常会被问到的点,所以今天来把线程池相关的知识详细的整理下。

什么是线程池?

线程池其实就是一个管理线程的池子,它可以帮我们创建、维护、销毁线程,类似于数据库连接池,在我们有任务需要执行时,我们不需要去手动的创建一个线程来执行这个任务,而是将任务交给线程池,由线程池内维护的线程来对任务进行处理。

为什么要用线程池?

  1. 线程的创建(需要开辟栈空间等操作)、销毁都需要浪费很多cPU资源,如果我们每个任务都重新创建线程,就会浪费大量资源,而引入线程池之后,我们就可以让线程复用,即一个线程完成一个任务后,可以继续去完成其它任务,就可以节省CPU的开销(可以参考数据库连接池)。
  2. 引入线程池让我们在执行任务时不必再关心线程创建、销毁的过程,让代码更加简介且便于维护。

线程池的状态

我们可以通过ThreadPoolExecutor来创建一个线程池,其中会用一个原子整数来表示线程池的状态和当前存活的线程数量(高3位表示线程池状态,低29位表示当前存活线程数量)。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

为什么要将两个信息存放到一个原子参数中?这是为了能让修改这两个信息只用一次原子操作就可以完成,保证了线程的安全性。

线程池分为以下状态:

image.png

线程池状态相关操作

-   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:放弃队列中最早的任务,本任务取而代之。

线程池的工作方式

  1. 当线程池刚被创建时,此时线程池内没有被创建的线程。
  2. 当任务到达时,若此时线程池内的线程数未达到核心线程数上限,且目前线程均处于工作状态,那么这时候线程池会为该任务创建一个新的核心线程来执行。等到该线程执行完该任务且没有新的任务时,这个线程不会被销毁。
  3. 当线程池内的核心线程数已到达上限(均在工作),且阻塞队列未满时,这时候新来的任务就会被放入堵塞队列里,等到有核心线程执行完任务后,就会到堵塞队列里拿新的任务来执行。
  4. 当线程池内的核心线程数已到达上限(均在工作),且阻塞队列已满时,此时若线程池内线程数未到达最大线程数,这时候线程池会创建一个紧急线程来处理该任务。当紧急线程执行完该任务后,会等待我们设置的最长空闲等待时间,若在该时间内有新的任务到来(依然满足堵塞队列已满的条件),这时候紧急线程就会去处理这个任务,若超过最大空闲等待时间仍没有新的任务,这个紧急线程就会被销毁掉。
  5. 当线程当线程池内的最大线程数已到达上限(均在工作),且阻塞队列已满时,这时候再来新的任务,就会触发拒绝策略。

线程池的常用方法

-   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 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量
-   混合型:可以将任务进行分类,分成两个线程池来处理。

参考文献

面试必备:Java线程池解析

黑马程序员全面深入学习Java并发编程,JUC并发编程全套教程