Java线程池实现原理(一)

背景

线程池是什么

线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,或者涉及到一些复杂线程操作需要重复开启线程。线程过多会带来额外的开销,包括创建线程的开销,调度线程的开销等等,同时也降低了计算机的整体性能。
线程池能维护多个线程,等待监督管理分配可并发执行的任务,这种做法一方面避免了处理任务时创建销毁线程开销的代价,另一方面也避免了线程数量膨胀的过分调度问题,保证了对内核的充分利用。
Java中对线程池的实现都是基于ThreadPoolExcutor类,它能带来一系列的好处:

  • 降低资源消耗: 通过池化技术重复利用已经创建的线程,降低线程创建和销毁造成的损耗
  • 提高响应速度: 任务到达时,无需等待线程创建就可以立即执行(前提是线程池中有可用的线程)
  • 提高线程的可管理性: 线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。不用开发者自己去维护这一整个流程。
  • 提供更多更强大的功能: 线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

线程池能解决什么问题

线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。这种思想也广泛应用于计算机的其它领域中:

  • 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
  • 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。例如Sql连接池
  • 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。例如String。

Java线程池核心设计于实现

总体设计

Java中的线程池核心实现类是ThreadPoolExecutor,首先分析UML类图,如下:

ThreadPoolExcutor UML类图

具体分析:

Executor: ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦,用户无需关注如何创建线程,如何调度线程来执行任务,用户只需要提供Runable对象,将任务的执行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。
ExecutorService:接口增加了一些能力:

  1. 扩充执行任务的能力,补充可以为一个或是一批异步任务生成Future方法
  2. 提供了管控线程池的方法,比如停止线程池的运行。

AbstractExecutorService:是上层的抽象类,将执行任务的流程串联起来,保证下层的实现只需关注一个执行任务的方法即可。
ThreadPoolExecutor:最下层的实现类实现最复杂的运行部分,一方面维护自身的生命周期,另一方面同时还需要管理线程和任务,使两者良好的结合从而执行并行任务。

下图是ThreadPoolExecutor的运行机制:

从图中可以看出来,线程池内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。

线程池的运行主要分成两部分:任务管理线程管理
任务管理:充当生成者的角色,当任务提交后,线程池会判断该任务后续的流转:

1. 直接申请线程执行该任务
2. 缓冲到队列中等待线程执行
3. 拒绝该任务

线程管理:线程管理部分是消费者,他们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务,线程就会被回收。

下一节继续按照下面三个部分去介绍线程池的运行机制:

  1. 线程池如何维护自身的生命周期
  2. 线程池如果管理任务
  3. 线程池如何管理线程