我理解的Java线程池

106 阅读7分钟

随着服务器性能的日益提升和程序对高吞吐量低延迟的不断追求,多线程编程成为对Java程序员的重要基础要求之一。在编程实践中,我们通常借助线程池来对多线程的任务进行调度管理。虽然用的不少,但是其实自己刚开始对线程池的理解也存在不少错误,只是因为程序并未真正运行到某种较高的并发量级才未造成线上故障。今天,通过这篇来总结一下我所理解的线程池。

池化思想

首先,线程池是池化思想的一种应用,在Java中,其他常见的池化思想的应用还有数据库连接池、对象池、缓存池等。池化思想,简单来说,就是对某种稀缺资源,使用一个池子来统一管理这类资源的实例,进而达到提高资源利率用、控制资源总量等目的。
线程池作为池化思想的一种应用,那么也具有池化带来的一些优点:

  • 统一管理线程对象,控制线程总量
  • 避免频繁创建和销毁线程带来的开销

相关类

我们平时使用线程池主要的类是java.util.concurrent.ThreadPoolExecutor,通过idea查看类依赖关系,如图所示:

类依赖关系、.png

最顶层的Executor接口中,只定义了一个execute方法,接收一个Runnable作为参数,这个接口解耦了调用线程和工作线程(真正执行Runnable的线程)。

ExecutorService接口继承了Executor接口,并添加定义了关闭线程池的shutdown方法用于管理线程池的生命周期,最重要的,是增加了返回Future的submit方法,这样让线程池执行子线程后,可以有返回值。

抽象类AbstractExecutorService实现了ExecutorService接口,对ExecutorService相关方法提供了默认实现。例如,对于submit方法,通过FutureTask包装Runnable任务,交给execute()方法执行,然后可以从该FutureTask阻塞获取执行结果。

ThreadPoolExecutor是我们平时使用线程池直接使用到的一个类,它继承了抽象类AbstractExecutorService。比较重要的是,它细化了线程池的生命周期管理,并通过一个AtomicInteger ctl来同时表达当前线程池状态和线程数量2个含义。之所以这么设计,因为在这个类中,有很多地方需要同时更新池状态和当前线程数,放在同一个原子变量里面比较容易保持同步更新,避免出现数据不一致,这种设计也可以借鉴到我们平时开发中,不过,这样的设计会带来一些代码阅读复杂度,也是有利有弊,使用前需要权衡。

线程池状态机

image.png

这5个状态在ThreadPoolExecutor类中有注释说明:

RUNNING

Accept new tasks and process queued tasks
线程池正常运行状态,接受新任务,持续处理任务队列里的任务

SHUTDOWN

Don't accept new tasks, but process queued tasks
不再接受新任务,要处理任务队列里的任务,调用shutdown()方法后进入此状态

STOP

Don't accept new tasks, don't process queued tasks, and interrupt in-progress tasks
不再接受新任务,也不再处理任务队列里的任务,中断正在执行的任务,调用shutdownNow()方法后进入此状态

TIDYING

All tasks have terminated, workerCount is zero, the thread transitioning to state TIDYING
线程池正在停止运行,终止所有任务,销毁所有工作线程,当线程池执行 terminated() 方法时,线程池进入 TIDYING 状态

TERMINATED

terminated() has completed
终止状态,这个状态是终态,所有的工作线程已经被销毁,所有的任务已经被清空或者执行完毕,terminated() 方法执行完毕之后线程池进入此状态

构造方法

ThreadPoolExecutor的构造方法和各个参数含义,应该是平时开发中定义线程池最常用到的知识,先来看一下构造方法定义

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
各个参数含义概览
参数含义
corePoolSize核心线程数量
maximumPoolSize最大线程数量
keepAliveTime非核心线程闲置情况下的存活时间
unitkeepAliveTime的单位
workQueue工作队列,用于存储暂未被工作线程处理的任务
threadFactory线程工厂,用于创建线程,不指定为默认线程工厂DefaultThreadFactory
handler拒绝策略

需要注意的是,虽然描述含义的时候,区分了核心线程和非核心线程,但是这个标记并不是在每个线程上,而是超过corePoolSize,就可以算非核心线程。

workQueue
  • ArrayBlockingQueue 这是平时开发定义线程池的时候常用的一个队列,基于数组实现的有界队列,初始化时必须指定容量参数,当积压的任务数超过这个容量的时候,线程池中线程数量如果还未达到maximumPoolSize,就会启动新的工作线程来执行任务
  • LinkedBlockingQueue 这是基于链表实现的无界队列,默认队列的长度是Integer.MAX_VALUE,也可以指定队列的长度,当队列满时进行阻塞操作,当然线程池中采用的是offer方法并不会阻塞线程,当队列满时则返回false,入队成功则则返回true
  • PriorityBlockingQueue 优先队列,也是无界的,特点是可以根据任务的优先级决定出队列的顺序
  • SynchronousQueue 这个容量为空的队列,本身不能积压任务进行蓄水,任务提交时有空闲线程就交给空闲线程处理,没有空闲则执行拒绝策略。
handler

向线程池新提交任务时,如果workQueue已经积压满了,且线程数也已经达到了maximumPoolSize,线程池既没有线程资源来处理,也没有存储资源来积压时,就会触发拒绝策略,主要有如下这4种:

  • DiscardPolicy 直接丢新任务,最简单粗暴的处理,因为这样会导致任务丢失,一般的开发场景不能接受,只有这个任务是否执行不重要的业务场景会使用到
  • DiscardOldestPolicy 丢弃队列最老任务,添加新任务。虽然比直接丢弃的策略好一些,但是一般的开发场景不能接受
  • CallerRunsPolicy 使用调用线程执行任务。这样直接使用调用线程来执行,利用最后的资源,那这个任务的处理实际上已经失去了多线程的特性
  • AbortPolicy 抛出异常。这是开发中最常见的拒绝策略,能够让调用方感知到问题的存在,但是也需要调用方做好异常处理的相关逻辑

任务处理过程

上面介绍了线程池构造方法的参数含义,这些参数会在线程池处理任务的过程中起作用,我们来看一下,向线程池中提交一个新任务,线程池的处理过程。

image.png

  1. 判断 线程数量 小于 corePoolSize,如果是,创建新的核心线程来处理任务,否则
  2. 判断 workQueue是否已经满了,如果还没满,则暂时积压到workQueue,否则
  3. 判断 线程数量 小于 maximumPoolSize,如果是,创建新的非核心线程来处理任务,否则
  4. 执行拒绝策略

这里有一点再次强调,图中的绿色和黄色框,分别对应第1步和第3步的创建新线程处理任务,虽然描述用了核心和非核心线程,但是其实在创建线程的时候,并没有标记哪个是核心,哪个是非核心,空闲了keepAliveTime后被结束的线程,也可能是绿色框这里创建出来的工作线程。