Java 线程池

线程的创建

在正式介绍线程池之前,先来聊聊创建线程有哪些方法!!

方法一:继承Thread类,重写run()方法

方法二:实现Runnable接口

如果研究过源码会发现方法一和方法二其实算一种,因为Thread实现了Runnable接口,方法一中重写的run()方法就是Runnable接口中抽象方法

方法三:实现Callable接口,并结合FutureTask

方法四:使用接下来要介绍的线程池!

线程池引入

大家肯定都多多少少听过「xxx池」,如:线程池、数据库连接池、HTTP 连接池等,它们都是一种基于池化思想来管理资源概念

如果没有线程池,那么我们在开发多线程程序时就需要随时随地的创建/销毁线程,而这样带来的开销也是极大的。所以线程池可以给我们带来的好处:

Executor 框架介绍

在传统的方式中,每创建一个线程时就绑定一个任务,也就是run()方法,当调用start()方法时就会执行任务,如下面代码所示:

这样的局限性在于一个线程只能执行一个任务,当任务执行完后线程的使命也就结束了,接着就应该被销毁,这也是为什么需要频繁创建/销毁线程的原因!!

而在 Executor 框架中将任务任务的执行解耦,也就意味着一个线程可以执行多个任务,我们只需要管理好一定数量的线程就可以实现复用,也避免了频繁的创建和销毁。这个框架分为三个部分:

1

注意:线程池实现类ThreadPoolExecutorExecutor框架最核心的类,所以后文如果没有明确说明均为ThreadPoolExecutor

线程池处理流程

线程池的核心在于当一个任务提交后,如何去执行它呢??

在介绍处理流程之前,先来看看四个概念:核心线程池、线程池、工作队列、饱和策略

当一个线程池被创建后,默认情况下是没有线程的,只是初始化了一些参数,如上面介绍的。当一个任务被提交到线程池后,主要的流程为:

为了整个流程更加清楚,配一张流程图和执行示意图:

2

线程池状态

注意:这里说的是线程池的状态,而不是线程的状态

线程池的运行状态并不是用户显示的设置,而是伴随着线程池的运行由内部来维护。线程池的内部由一个整型原子变量来维护两个值:运行状态 (runState) & 工作线程数量 (workerCount)

下面是ThreadPoolExecutor的部分源码:

根据上面代码可以看出线程池一共分为 5 种运行状态:

状态之间的转换如下图:

3

技巧:由于状态是递增的,在源码中可以看到大量用< 0判断线程池是否处于运行,用>= 0判断线程池是否处于半死不活的状态

阻塞队列

阻塞队列用来存储没有分配线程的任务,类似于是一个缓冲区,也可以看作是生产者-消费者模型,任务的创建者作为生产者向队列中添加任务,任务的执行者作为消费者从队列中取出任务

使用不同的队列可以实现不一样的任务存取策略,下面介绍一些常见的阻塞队列:

饱和策略

如上文所讲,当核心线程池、任务队列、线程池均满了时,新提交的任务会直接交给饱和策略处理,一共有四种:

源码分析

前提:线程池实现类ThreadPoolExecutorExecutor框架最核心的类,下面主要分析ThreadPoolExecutor类的源码

初始化

ThreadPoolExecutor的构造函数一共四个,主要就是初始化一些参数,这里介绍其中最全的一个:

execute()

当创建好线程池后就会调用execute()提交任务,源码的流程和上面介绍的 线程池处理流程 完全吻合!!下面是对execute()的源码分析:

addWorker()

execute()的源码中可以看到调用了多次addWorker()方法,它是添加一个工作线程 (包括创建和启动线程两个步骤)。下面是对addWorker()的源码分析:

配上一张执行流程图:

4

Worker

addWorker()的源码中可以看出线程池将任务Runnable封装成了一个Worker实例,下面来看看Worker的庐山真面目

runWorker()

addWorker()中会调用t.start()启动创建的线程,后续会执行Worker中的run()方法,而run()方法中调用runWorker()方法。下面来看看runWorker()的源码:

getTask()

runWorker()中线程会不断尝试从任务队列中获取可执行任务,这就是通过调用getTask()实现,下面看看它的源码:

processWorkerExit()

runWorker()中,如果线程获取不到任务时会执行processWorkerExit(),它主要负责终结当前工作线程,下面看看它的源码:

到此为止,「向线程池提交任务 -> 创建工作线程 -> 拉取任务真正执行 -> 无任务时终结线程」这一整个流程就分析完了,给出一张完整的流程图:

5

shutdown()

为了补上前面挖的坑:Worker 类继承 AQS 实现不可重入锁可以用来判断线程是否在执行任务

任何时候都只允许一个线程获取该锁,且同一线程不可重入。runWorker()中可以看到线程执行任务时会上锁w.lock(),当我们可以可以获取该锁,说明线程空闲

调用shutdown()方法会使线程池处于 SHUTDOWN 状态,不再接收新提交的任务,但能处理任务队列中已保持的任务,所以该方法中需要中断空闲的线程,下面看看它的源码:

Runnable & Callable & execute & submit 比较

Runnable VS Callable

Runnable 接口不会返回结果或抛出异常;Callable 可以返回结果和抛出异常

execute VS submit

execute()方法用于提交不需要返回的任务,无法判断任务执行的成功与否

submit()方法用于提交需要返回值的任务,通过一个Future类型对象接收

几种常见的内置线程池

下面要介绍的常见内置线程池都是基于ThreadPoolExecutorScheduledThreadPoolExecutor创建的,只是提前设置好了一些参数让线程池具有某些特性,我们完全可以直接使用构造函数自己创建

FixedThreadPool

FixedThreadPool 被称为可重用固定线程数的线程池

不推荐使用的原因

SingleThreadExecutor

SingleThreadExecutor 是只有一个线程的线程池

不推荐使用的原因

CachedThreadPool

CachedThreadPool 是一个会根据需要创建新线程的线程池

不推荐使用的原因

ScheduledThreadPool

ScheduledThreadPool 用来在给定的延迟后运行任务或者定期执行任务。这个在实际项目中基本不会被用到,也不推荐使用

实践出真知

线程池面临的核心问题在于:如何配置线程池的参数,主要是 corePoolSize、maximumPoolSize、任务队列、饱和策略

场景一:由于没有预估好请求流量,导致最大核心线程数设置偏小,大量抛出RejectedExecutionException异常

场景二:由于任务队列设置过长,最大线程数失效,导致请求数量增加时大量任务堆积在队列中,任务执行时间过长

回到上面的问题:如何配置线程池的参数,这里给出一种解决方案:动态调整参数

参考文章