1 线程简介
1.1 什么是线程
在介绍线程之前,先来了解一下程序和进程的概念。
程序,是算法和数据结构及其组织形式的一种描述。在操作系统中,后缀为.exe的文件都是一个程序。程序是”死“的,是一个静态代码文件。当你双击执行这个文件的时候,就会得到一个相对应的进程,所以,进程是”活“的,是正在被执行的活动实体。
进程(Process),是计算机中的程序关于某些数据集合上的一次运行活动,它是操作系统进行资源分配和调度的基本单位。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体。但是,在现代面向线程设计的计算机结构中,进程是装载一组线程的容器。
操作系统在运行一个程序时,会为其创建一个进程。例如:启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小执行单元是线程,也叫作轻量级进程(Light Weight Process,LPW)。在一个进程中可以创建多个线程,这些线程都拥有各自的寄存器、程序计数器、栈、局部变量等属性,并且能够访问存在于进程中的共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。
一个Java程序从main( )方法开始执行,然后按照既定的代码逻辑顺序执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main( )方法的是一个名叫main的线程,我们称之为主线程。下面使用Java虚拟机线程管理对象来查看一个普通的Java程序包含哪些线程,示例代码如下:
public class PoolMultiThreadTest {
public static void main(String[] args) {
//获取Java线程管理Bean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
//获取线程堆栈信息
ThreadInfo[] allThreads = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo oneThread : allThreads) {
System.out.println("[" + oneThread.getThreadId() + "]" + oneThread.getThreadName());
}
}
}
输出结果如下所示。可以看到,一个Java程序的运行,不仅仅是main( )线程的运行,而是main( )线程和多个其他线程的同时执行。
[4]Signal Dispatcher // 信号分发器线程
[3]Finalizer // 调用对象finalize方法的线程
[2]Reference Handler // 清除引用Reference的线程
[1]main // 主线程,用户程序的执行入口
1.2 为什么要使用多线程
(1)更多的处理器核心
随着处理器上的核心数量越来越多,现在的大多数计算机都很擅长并行计算,而处理器性能的提升方式,也逐渐从追求更高的主频向追求更多的处理器核心发展,如何利用好处理器上的多个计算核心也成了现在的主要问题。线程是现代操作系统调度的基本单位,一个程序作为一个进程来运行,进程在运行过程中能够创建多个线程,而一个线程在一个时刻只能运行一个处理器核心上。试想,一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法提高程序的执行效率。相反,如果程序使用多线程技术,将计算逻辑分配到不同的处理器核心上并行执行,那么就会大幅度减少程序的处理时间。
(2)期望更快的响应时间
在一些业务逻辑复杂的场景下,比如页面渲染场景,比如:用户发起一个实时请求,想要查看商品列表的信息,那么我们就需要将商品维度的一系列信息,包括商品价格、剩余库存、商品图片、优惠力度、生产厂商等等数据按照商品维度逐一聚合起来展示给用户。从用户体验角度看,这个查询结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查询这个商品了。
而面向用户做数据聚合通常非常复杂,可能会伴随接口调用之间的级联等情况,此时,我们可以选择使用多线程方式来处理,将接口调用封装成多个小粒度任务并行执行,缩短总体响应时间。
1.3 线程的基本操作
1.3.1 新建线程
1.3.1.1 继承Thread类
(1)在Java语言中,负责实现线程功能的是java.lang.Thread类,可以通过继承Thread类来创建一个线程对象,每个线程对象都是通过重载Thread类中的run( )方法来完成其业务操作,run( )方法被称为线程体。
(2)通过调用Thread类的start( )方法来启动一个线程。
注意:Java支持单继承多实现,所以继承本身就是一种珍贵的资源,当我们的类继承了一个基类(比如midas-shopdiy-web中的BaseAction),就无法再继承Thread类了,所以,这种方式的使用场景较少。
public class PoolThreadTest {
public static void main(String[] args) {
MyThread oneThread = new MyThread("oneThread"); //创建1个线程
oneThread.start(); //启动1个线程,执行业务逻辑
}
}
//定义一个线程对象
class MyThread extends Thread {
public MyThread(String oneThread) {
super(oneThread);
}
@Override
public void run() {
for (int x = 0; x < 1000; x++) {
System.out.println(Thread.currentThread().getName().concat(("--run--").concat(String.valueOf(x))));
}
}
}
1.3.1.2 实现Runnable接口
在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了实现Thread类的缺点,即:在实现Runnable接口的同时,还可以继承某个类。
public class PoolRunnableTest {
public static void main(String[] args) {
ExecutorService pool = Executors.newSingleThreadExecutor(); // 创建一个线程池对象
pool.submit(new MyRunnable("oneThread")); // 线程池执行Runnable对象
pool.shutdown(); // 结束线程池
}
}
//定义一个线程对象
class MyRunnable implements Runnable {
private String oneThread;
public MyRunnable(String oneThread) {
this.oneThread = oneThread;
}
@Override
public void run() {
for (int x = 0; x < 1000; x++) {
System.out.println(oneThread.concat("--run--").concat(String.valueOf(x)));
}
}
}
1.3.1.3 实现Callable接口
前面两种实现多线程的方式都有一个缺陷:在执行完任务之后无法获取业务执行的结果。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
所以,自从Java 1.5开始,就提供了新的方式:Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
public class PoolCallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(1); // 创建一个线程池对象
Future<Integer> f1 = pool.submit(new MyCallable(100)); // 线程池执行Callable对象
Integer i1 = f1.get(); // 阻塞获取线程执行的结果
System.out.println(i1); // 对线程的执行结果做其他处理
pool.shutdown(); // 结束线程池
}
}
//定义一个线程对象,功能:线程求和案例
class MyCallable implements Callable<Integer> {
private int number;
public MyCallable(int number) {
this.number = number;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int x = 1; x <= number; x++) {
sum += x;
}
return sum;
}
}
1.3.2 启动线程
线程对象在初始化完成之后,调用线程对象的start( )方法就可以启动该线程。
(1)调用start( )方法启动线程,两条执行路径(主线程与子线程),真正实现了多线程运行。start( )方法只是将子线程由新生态转为就绪态,而不是运行态。何时真正运行,还需要等待操作系统分配CPU时间片。
(2)run( )方法只是一个普通的方法而已,如果通过线程对象直接调用run( )方法,程序中依然只有主线程这一个执行路径,业务逻辑还是需要顺序执行,并没有达到多线程的目的。
1.4 线程的生命周期
Java线程在运行的生命周期中可能处于下表中的6种不同状态。在给定的某个时刻,线程只能处于其中的一个状态。
状态名称 | 说明 |
---|---|
NEW | 初始状态:线程对象被创建,但是还没有调用start( )方法 |
RUNNABLE | 运行状态:Java线程将操作系统中的“就绪态和运行态”两种状态笼统地称作”运行中“ |
BLOCKED | 阻塞状态:表示线程因为获取锁的缘故被阻塞了 |
WAITING | 等待状态:表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作来唤醒(中断或通知) |
TIME_WAITING | 超时等待状态:该状态不同于WAITING,它表示等待中的线程可以在指定的时间点自行返回,无需一直等待下去,可避免造成饥饿状态(优先级也会造成这种现象) |
TERMINATED | 终止状态:表示当前线程已经顺利执行完毕 |
6中线程状态的切换路径图:
2 线程池简介
首先,虽然与进程相比,线程是一种轻量级的工具,但是线程的创建与关闭仍然需要花费时间,如果为每一个任务都创建一个线程的话,有可能会出现创建和关闭线程所消耗的时间大于该线程真实做业务逻辑工作所消耗的时间的情况,反而会得不偿失。
其次,线程本身需要占用CPU时间和内存空间,大量的线程会抢占系统内存资源,如果处理不当,可能会造成Out Of Memeory错误,即使没有,大量的线程回收也会给GC带来很大压力,延长GC停顿时间。
因此,对线程的使用需要有一个度,在有限的范围内适当增加线程数量可以显著提高系统的吞吐量。但是一旦超出这个范围,大量的线程只会拖慢应用系统。
2.1 什么是线程池
举例:数据库连接池就是种典型的池化思想的体现。为了避免每次数据库查询都重新创建和销毁数据库连接,可以使用数据库连接池维护一些已经创建好的数据库连接对象,让它们长期保持一个激活状态,进行复用。当需要查询数据时,并不是创建一个新的数据库连接,而是直接从连接池中获取一个可用的连接。反之,当数据查询结束需要关闭连接的时候,也不是真正的把这个连接关闭,而是将这个连接归还给连接池即可。通过这种池化方式,可以节约很多创建和销毁对象的时间。
线程池,也是类似的做法。在线程池中,总有那么几个活跃线程。当你需要使用线程执行任务时,可以从池子中拿一个空闲线程;当任务执行结束之后,并不是着急关闭线程,而是将这个线程归还给池子,便于其他人使用。如下图所示。
2.2 为什么要使用线程池
线程池(Thread Pool),是一种基于池化思想管理线程的工具。合理的使用线程池,可以带来3个好处:
(1)降低资源消耗。通过重复使用已经创建好的线程对象循环执行任务,避免了为每个任务都创建和销毁一个线程带来的时间和空间消耗。
(2)提高响应速度。当任务达到时,任务可以不需要等待线程的创建就能立即被执行。
(3)提高线程的可管理性。线程是稀缺资源,如果不加限制的创建,不仅会大量消耗系统资源,还会降低系统的稳定性,但是使用线程池,就可以进行统一分配、调优、监控。
2.3 线程池的生命周期
线程池的运行状态,总共有五种:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED。
运行状态 | 状态描述 |
---|---|
RUNNING | 运行状态:说明线程池能够接受新提交的任务,并且也能够处理阻塞队列中的任务 |
SHUTDOWN | 关闭状态:说明线程池不能再接受新提交的任务,但是可以继续处理阻塞队列中的已经保存的任务 |
STOP | 立即停止状态:说明线程池不能再接受新提交的任务,也不会处理阻塞队列中的任务,同时会中断正在处理中的任务 |
TIDYING | 当所有的任务都已终止,”任务数量”变为0,线程池会变为TIDYING状态,此时内部会执行函数terminated( ) |
TERMINATED | 在函数terminated( )执行结束之后进入该状态 |
线程池的五种运行状态,并不是用户显式设置的,而是伴随着线程池的运行,由JDK内部来维护并流转。这五种状态如何进行存储呢?
通过源码可以知道,线程池内部使用一个原子类型整型变量 ctl 同时维护两个值:运行状态(runState)和工作线程数量 (workerCount),其中:runState占用高3位,workCount占用低29位。
相关源代码解析如下:
// ctl:一个变量同时存储运行状态(runState)和工作线程数量 (workerCount),其中runState占用高3位,workCount占用低29位。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; //workerCount使用的位数:32-3=29位
private static final int CAPACITY = (1 << COUNT_BITS) - 1; //workerCount低29位,最大值=536870911,二进制补码: 00011111_11111111_11111111_11111111
private static final int RUNNING = -1 << COUNT_BITS; //runState高3位,二进制补码:值=-536870911,11100000_00000000_00000000_00000000
private static final int SHUTDOWN = 0 << COUNT_BITS; //值=0,00000000_00000000_00000000_00000000
private static final int STOP = 1 << COUNT_BITS; //值=1,00100000_00000000_00000000_00000000
private static final int TIDYING = 2 << COUNT_BITS; //值=2,01000000_00000000_00000000_00000000
private static final int TERMINATED = 3 << COUNT_BITS; //值=3,01100000_00000000_00000000_00000000
private static int runStateOf(int c) {
return c & ~CAPACITY; //获取runState,即保留ctl的高3位,后29位置0
}
private static int workerCountOf(int c) {
return c & CAPACITY; //获取workerCount,即保留ctl的低29位,高3位置0
}
private static int ctlOf(int rs, int wc) {
return rs | wc; //初始化设置ctl(或操作)
}
线程池自身的状态和线程数量都维护在一个原子变量 ctl 中,目的不是为了减少存储空间,而是将线程池状态与工作线程个数合二为一,这样就可以用一次CAS原子操作进行修改赋值,更容易保证在多线程环境下保证运行状态和线程数量的统一。
public class PoolRunnableTest {
public static void main(String[] args) {
// 创建一个线程池对象
ExecutorService pool = Executors.newSingleThreadExecutor();
// 线程池执行Runnable对象
pool.submit(new MyRunnable("oneThread"));
// 结束线程池
pool.shutdown();
}
}
class MyRunnable implements Runnable {
private String oneThread;
public MyRunnable(String oneThread) {
this.oneThread = oneThread;
}
@Override
public void run() {
for (int x = 0; x < 1000; x++) {
System.out.println(oneThread.concat("--run--").concat(String.valueOf(x)));
}
}
}
(1)新建线程池时,ctl 的变量如下图:
(2)submit一个runnalbe以后,ctl 值如下图:
由于只使用了32位中的29位存储工作线程workerCount,它的最大值是536870911,如果代码中会有超过536870911个线程加入线程池中并允许开始执行,就要注意内存溢出OOM的风险。
五种状态中,只有RUNNING状态时最高位为1,为负数,所以检查线程池状态只需要时通过判断ctl是否大于0,就可以知道是否处于此状态。其他4种状态最高位为0,都为正数。
2.4 如何使用线程池:JDK对线程池的支持
2.4.1 Executor框架的类结构
Executor框架主要是由三大部分组成:
(1)任务:也就是工作单元,包括被执行任务所需要实现的接口:Runnable接口和Callable接口。
对于实现了Runnable接口或者Callable接口的类都可以作为一个任务单元,被ThreadPoolExecutor和ScheduledThreadPoolExecutor去执行。
(2)任务的执行:包括任务执行机制的核心接口Executor,Executor是整个线程池框架的基础,以及继承自Executor的ExecutorService接口。其中,框架中有两个关键类实现了ExecutorService接口:
ThreadPoolExecutor类:常规意义上的线程池,用来执行被提交的任务。
ScheduledThreadPoolExecutor类:具有特殊功能的线程池,可以在给定的时间延迟后执行任务,也可以在周期性执行定时任务。
(3)异步计算的结果:包括Future接口和实现了Future接口的FutureTask类。
(lExecutor的UML类图)
2.4.2 Executor框架的使用示意图
首先,主线程需要创建实现了Runnable接口或者Callable接口的任务对象。线程池工具类Executors可以把一个Runnable对象封装成一个Callable对象:Executors.callable( Runnable task )、Executors.callable( Runnable task,Object result )。
然后,可以直接把Runnable对象或者Callable对象提交给ExecutorService执行:ExecutorService.executor( Runnable task )、ExecutorService.submit( Callable task )。如果执行ExecutorService.submit( ... ),ExecutorService将返回一个实现Future接口的对象:FutureTask。
最后,主线程可以执行FutureTask.get( )方法来等待任务执行完毕,以获取执行结果。主线程也可以执行FutureTask.cancle( )方法来取消此次任务的执行。
2.4.3 ThreadPoolExecutor详解
(ThreadPoolExecutor的UML类图)
(1)ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程、如何调度线程来执行任务,用户只需要提供Runnable对象或者Callable对象,将任务的运行逻辑提交给执行器( void execute(Runnable command); ),由Executor框架完成线程的创建和任务的执行。
(2)ExecutorService接口增加了一些能力:1)扩充执行任务的能力,补充可以为异步任务生成Future的方法。2)提供了管理线程池的方法,比如停止线程池的运行等。
(3)AbstractExecutorService是上层接口的抽象类,将执行任务的流程串联起来,保证下层的实现只需关注一个执行任务的方法submit( )或executor( )即可。
(4)最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor类一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者结合起来从而并行执行任务。
ThreadPoolExecutor类是如何运行的,如何同时维护线程和执行任务的?它的运行机制是如下图所示。
线程池在内部实际上构建了一个生产者-消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。
任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。
线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
2.4.4 Executors工具类创建线程池
在Executor框架中,Executors工具类扮演着线程池工厂的角色,通过Executors工具类可以获得一个具有特定功能的线程池。目前,主要提供了以下五种类型的线程池。
public static ExecutorService newFixedThreadPool(int nThreads) //返回一个固定线程数量的线程池
public static ExecutorService newSingleThreadExecutor() //返回一个只有一个线程的线程池
public static ExecutorService newCachedThreadPool() //返回一个可根据实际情况自动调整线程数量的线程池(自动新建/自动销毁线程)
public static ScheduledExecutorService newSingleThreadScheduledExecutor() //返回一个线程对象,可以在给定延迟之后执行,也可以周期性的执行某个任务
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) //返回多个线程对象,可以在给定延迟之后执行,也可以周期性的执行某个任务
类型 | 池内线程数量 | 池内线程类型 | 处理特点 | 应用场景 | 详细描述 |
---|---|---|---|---|---|
定长线程池 newFixedThreadPool | 固定 | 核心线程 | 1.核心线程处于空闲状态时也不会被回收 2.当所有线程都处于忙碌状态时,新任务会进入等待状态,直到有线程空闲出来 3.任务队列无大小限制 | 控制线程最大并发数 | 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,在提交新任务,任务将会进入等待队列中等待。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。内部使用了无限容量的LinkedBlockingQueue阻塞队列来缓存任务,任务如果比较多,如果处理不过来,会导致队列堆满,引发OOM。 |
单线程化线程池 newSingleThreadExecutor | 1个 | 核心线程 | 1.保证所有任务按照在一个线程中顺序执行(相当于串行执行任务) 2.不需要处理线程同步问题 | 单线程 | 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。内部使用了无限容量的LinkedBlockingQueue阻塞队列来缓存任务,任务如果比较多,单线程如果处理不过来,会导致队列堆满,引发OOM。 |
可缓存线程池 newCachedThreadPool | 不固定,无限制 | 非核心线程 | 1.优先利用空闲线程处理新任务(线程复用) 2.无线程空闲时会创建新线程立即执行任务 3.灵活回收线程(超时机制60s,全部回收时几乎不占用系统资源) | 执行数量多、耗时少的任务 | 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒处于等待任务到来)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池的最大值是Integer的最大值(2^31-1)。内部使用了SynchronousQueue同步队列来缓存任务,此队列的特性是放入任务时必须要有对应的线程获取任务,任务才可以放入成功。如果处理的任务比较耗时,任务来的速度也比较快,会创建太多的线程引发OOM。 |
定时线程池 newScheduledThreadPool | 核心线程数:固定 非核心线程数:无限制 | 核心线程 & 非核心线程 | 1.当非核心线程空闲时,会被回收 | 执行定时或周期性任务 | 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 |
对于这五个核心的线程池,虽然看起来创建的线程池有着各不相同的功能特点,但是其内部实现均使用了ThreadPoolExecutor类的构造方法。下面详细介绍一下ThreadPoolExecutor类的构造方法。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {......}
参数名称(7个) | 解释说明 |
---|---|
corePoolSize | 核心线程数量,重要参数,它表示线程池中常驻的线程数量最多是多少。在创建线程池后,默认情况下,线程池中并没有任何线程,而是等待新任务到来之后才会创建线程去执行任务。 注意:主动调用方法prestartAllCoreThreads( )或者prestartCoreThread( )除外。 |
maximumPoolSize | 最大线程数量,非常重要的参数,它表示线程池中最多能够创建多少个线程。注意:maximumPoolSize >= corePoolSize |
keepAliveTime | 存活时间,它表示当线程池中的线程数大于corePoolSize时,多出来的非核心空闲线程最多能够保持多长时间为激活状态。 注意:默认情况下,keepAliveTime只针对非核心线程有作用,对于核心线程无作用。但是,如果主动调用了allowCoreThreadTimeOut( boolean )方法,对于线程池中的核心与非核心线程,keepAliveTime参数都会起作用,直到线程池中的线程数减少至0。 |
TimeUnit | 存活时间的单位,共有7种类型的取值:天、小时、分钟、秒、毫秒、微秒、纳秒。 |
BlockingQueue | 任务缓冲队列,是一个阻塞队列接口,用来存储已经被提交但是等待被执行的任务。这个参数也很重要,会对线程池的运行产生重大影响。 |
ThreadFactory | 线程工厂,主要用来创建新线程,通常情况下,使用默认的即可。 |
RejectedExecutionHandler | 饱和策略,它表示当提交的任务太多以至于超出系统的实际承受能力时(线程池中的线程都在忙碌,而且缓冲队列已经满员),如何拒绝任务。JDK内置四种策略。 |
2.4.5 线程池执行机制
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务如何执行都是在这个阶段决定并且完成的。了解了这部分内容,就相当于了解了线程池的核心运行机制。
所有的任务调度都是通过executor方法完成的,这部分完成的工作主要是:检查线程池的运行状态、运行线程数、运行策略,从而决定接下来执行的流程,是直接申请线程执行,还是缓冲任务到缓冲队列中去,还是直接拒绝掉该任务。
它的主要执行流程描述如下:
(1)首先,检测线程池的运行状态,如果不是RUNNING,则直接拒绝。注意:线程池需要保证在RUNNING状态下才能执行任务。
(2)如果workerCount(工作线程数量) < corePoolSize(线程池核心数量),则创建并启动一个线程来执行新提交的任务。
(3)如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
(4)如果workerCount >= corePoolSize && workerCount < maximumPoolSize(线程池最大线程数量),且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
(5)如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据饱和策略来处理该任务,默认的处理方式是直接抛出异常。
2.4.5.1 任务缓冲
任务缓冲,是线程池能够进行任务管理的核心部分。可以这样认为,线程池的本质是对任务和线程的综合管理,而做到这一点最重要的思想就是将任务和线程进行解耦,以此来做后续的任务分配工作。为了不让两者直接进行关联,线程池中是通过阻塞队列来实现任务和线程之间的解耦。阻塞队列,通常用在生产者-消费者模型的场景下,它其实就是生产者存放元素的容器,而消费者从容器中获取元素。
下图展示了:线程1向阻塞队列中添加元素,线程2从阻塞队列中获取元素。
JDK内部提供了四种常用的阻塞队列。
任务队列 | 解释说明 |
---|---|
ArrayBlockingQueue(int capacity) | 基于数组的有界任务队列,构造函数有一个参数,使用这个参数事先指定队列的容量,按照FIFO原则对任务进行排序 |
LinkedBlockingQueue | 基于链表的无界任务队列,默认容量是Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQueue,按照FIFO原则对任务进行排序 |
SynchronizeQueue | 直接提交的任务队列,没有容量,通常和newCachedThreadPool类型的线程池配合使用,需要设置很大的maximumPoolSize,否则很容易就触发饱和策略 |
PriorityBlockingQueue | 具有优先级的任务队列,会按照任务本身优先级高低的原则对任务进行排序,总是先执行优先级别较高的任务,有可能会产生任务饥饿现象 |
2.4.5.2 任务拒绝
ThreadPoolExecutor类构造方法的最后一个参数指定了饱和策略。也就是当任务数量超过系统实际承载能力时,该如何处理新添加的任务?这时就需要使用到饱和策略了。
饱和策略,可以说是系统超负荷运行时的一种补救措施,通常是由于系统压力太大导致的,也就是:线程池中的线程已经使用完了,所有的线程都在工作,同时,任务队列也都被排满了,再也塞不下新的任务对象了。这时,就需要提供一套安全机制来合理处理这个问题。
JDK内部提供了四种常用的饱和策略。
饱和策略 | 解释说明 |
---|---|
AbortPolicy | 会丢弃最新被提交的任务,同时抛出异常,阻止系统继续正常工作。这是Executor框架默认使用的饱和策略,真实使用时,需要注意对异常的捕捉并给出处理手段 |
DiscardPolicy | 默默的丢弃最新被提交的无法被处理的任务,但是不抛出异常。如果任务允许少量丢失,则可以使用这种策略 |
DiscardOldestPolicy | 会丢弃缓冲队列中最老的一个任务,并且再次尝试提交任务,通常情况下在第二次提交时就会进入缓冲队列,等待被执行 |
CallerRunsPolicy | 只要线程池未被关闭,该策略会在调用者线程中直接执行当前被提交的最新任务。虽然这样做不会丢弃任务,但是任务提交的性能很有可能受到影响,急剧下降 |
2.4.5.3 线程池工作原理部分源码分析
(1)java.util.concurrent.ThreadPoolExecutor#execute方法
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//clt记录着runState和workerCount
int c = ctl.get();
/*
* (1)workerCountOf方法取出低29位的值,表示当前活动的工作线程数;
* (2)如果当前活动线程数小于corePoolSize,则新建一个线程放入线程池中,并把任务添加到该线程中。
*/
if (workerCountOf(c) < corePoolSize) {
/*
* addWorker中的第二个参数表示限制添加线程的数量 是根据corePoolSize来判断还是maximumPoolSize来判断:
* (1)如果为true,根据corePoolSize来判断
* (2)如果为false,则根据maximumPoolSize来判断
*/
if (addWorker(command, true))
return;
c = ctl.get();//如果添加失败,则重新获取ctl值
}
/*
* 如果当前线程池是运行状态并且任务添加到队列成功
*/
if (isRunning(c) && workQueue.offer(command)) {
// 重新获取ctl值
int recheck = ctl.get();
// 再次判断线程池的运行状态:(1)如果不是运行状态,由于之前已经把command添加到workQueue中了,这时需要移除该command,
// 执行过后再通过handler使用拒绝策略对该任务进行处理,整个方法就返回了。
if (! isRunning(recheck) && remove(command))
reject(command);
/*
* (2)如果线程池是运行状态:获取线程池中的有效线程数,如果数量是0,则执行addWorker方法
* 这里传入的参数表示:
* 1. 第一个参数为null,表示在线程池中新建一个线程,但不去启动;
* 2. 第二个参数为false,将线程池的有限线程数量的上限设置为maximumPoolSize,添加线程时根据maximumPoolSize来判断;
* (3)如果线程池是运行状态:如果判断workerCount大于0,则直接返回,在workQueue中新增的command会在将来的某个时刻被执行。
*/
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
/*
* 如果执行到这里,有两种情况:
* 1. 线程池已经不是RUNNING状态;
* 2. 线程池是RUNNING状态,但workerCount >= corePoolSize并且workQueue已满。
* 这时,再次调用addWorker方法,但第二个参数传入为false,将线程池的有限线程数量的上限设置为maximumPoolSize;
* 如果失败则拒绝该任务。
*/
else if (!addWorker(command, false))
reject(command);
}
(2)java.util.concurrent.ThreadPoolExecutor#addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
// 跳转标识
retry:
for (;;) {
int c = ctl.get();
// 获取线程池的运行状态
int rs = runStateOf(c);
/*
* 这个if判断:
* 如果rs >= SHUTDOWN,则表示此时不再接收新任务;
* 紧接着判断以下3个条件,只要有1个不满足,则返回false:
* 1)rs == SHUTDOWN,这时表示关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务
* 2)firsTask为空
* 3)阻塞队列不为空
* 首先考虑rs == SHUTDOWN的情况
* 这种情况下不会接受新提交的任务,所以在firstTask不为空的时候会返回false;
* 然后,如果firstTask为空,并且workQueue也为空,则返回false,
* 因为队列中已经没有任务了,不需要再添加线程了。
*/
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
// 获取线程数
int wc = workerCountOf(c);
// 如果wc超过CAPACITY,也就是ctl的低29位的最大值(二进制是29个1),返回false;
// 这里的core是addWorker方法的第二个参数:如果为true表示根据corePoolSize来比较,如果为false则根据maximumPoolSize来比较。
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 尝试增加workerCount,如果成功,则跳出第一个for循环!!!
if (compareAndIncrementWorkerCount(c))
break retry;
// 如果增加workerCount失败,则重新获取ctl的值
c = ctl.get(); // Re-read ctl
// 如果当前的运行状态不等于rs,说明线程池的状态已被改变,返回第一个for循环继续执行!!!
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 根据firstTask来创建Worker对象
w = new Worker(firstTask);
// 每一个Worker对象都会创建一个线程
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
// rs < SHUTDOWN表示是RUNNING状态;
// 如果rs是RUNNING状态或者rs是SHUTDOWN状态并且firstTask为null,向线程池中添加线程。
// 因为在SHUTDOWN时不会在添加新的任务,但还是会执行workQueue中的任务
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// workers是一个HashSet
workers.add(w);
int s = workers.size();
// largestPoolSize记录着线程池中出现过的最大线程数量
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
// 启动线程!!!
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
(3)java.util.concurrent.ThreadPoolExecutor.Worker类
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
//线程对象需要完成的业务逻辑功能在这里被调用
public void run() {
runWorker(this);
}
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
(4)java.util.concurrent.ThreadPoolExecutor#runWorker方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 获取第一个任务
Runnable task = w.firstTask;
w.firstTask = null;
// 允许中断
w.unlock(); // allow interrupts
// 是否因为异常退出循环
boolean completedAbruptly = true;
try {
// 如果task为空,则通过getTask()来获取任务,直到获取到任务为止
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
//在这里执行真正的任务:我们想要完成的业务逻辑
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
(5)java.util.concurrent.ThreadPoolExecutor#getTask方法
private Runnable getTask() {
// timeOut变量的值表示上次从阻塞队列中取任务时是否超时
boolean timedOut = false;
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
/*
* 如果线程池状态rs >= SHUTDOWN,也就是非RUNNING状态,再进行以下判断:
* (1)rs >= STOP,线程池是否正在stop;
* (2)阻塞队列是否为空。
* 如果以上条件满足,则将workerCount减1并返回null。
* 因为如果当前线程池状态的值是SHUTDOWN或以上时,不允许再向阻塞队列中添加任务。
*/
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// timed变量用于判断是否需要线程做超时控制:
// allowCoreThreadTimeOut,默认是false,也就是核心线程不允许进行超时控制;
// wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量,对于超过核心线程数量的这些线程,需要进行超时控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/*
* wc > maximumPoolSize的情况是因为可能在此方法执行阶段同时执行了setMaximumPoolSize方法;
* timed && timedOut 如果为true,表示当前操作需要进行超时控制,并且上次从阻塞队列中获取任务发生了超时
* 接下来判断,如果有效线程数量大于1,或者阻塞队列是空的,那么尝试将workerCount减1;
* 如果减1失败,则返回重试。
* 如果wc == 1时,也就说明当前线程是线程池中唯一的一个线程了。
*/
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
/*
* 根据timed来判断,如果为true,则通过阻塞队列的poll方法进行超时控制,如果在keepAliveTime时间内没有获取到任务,则返回null;
* 否则,通过take方法,如果这时队列为空,则take方法会阻塞直到队列不为空。
*/
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
// 获取到任务,并且返回这个任务
return r;
timedOut = true;//如果 r == null,说明已经超时,timedOut设置为true
} catch (InterruptedException retry) {
// 如果获取任务时当前线程发生了中断,则设置timedOut为false并返回循环重试
timedOut = false;
}
}
}
2.4.6 线程池的关闭
ThreadPoolExecutor类提供了两种关闭线程池的方法。
关闭线程池 | 解释说明 |
---|---|
shutdown( ) | 平缓关闭策略: 如果调用了shutdown()方法,则线程池处于SHUTDOWN状态。此时线程池不能够接受新的任务,但是,它会等待任务队列中的所有任务执行完毕 (1)响应性低(2)安全性高 |
shutdownNow( ) | 立刻关闭策略: 如果调用了shutdownNow()方法,则线程池处于STOP状态。此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务,并返回队列中尚未执行的任务 (1)响应性高(2)安全性低(因为shutdownNow方法会立刻停止执行中的任务,如果不记录这个未完成的任务,将会造成这个任务的丢失) |
如何选择呢?
至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常情况下,我们都是调用 shutdown( ) 来关闭线程池。如果任务不一定非要执行完就可以关闭,也能够容忍一定程度的任务丢失,则可以调用 shutdownNow( )。
3 如何合理设置线程池参数
3.1 阿里巴巴Java开发手册的建议
Executors工具类是一个线程池工厂类,提供了比较快捷的方式来创建线程池,比如:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
} ...
但是,《阿里巴巴Java开发手册》中明确提出了不允许使用Executors工具类去创建线程池,而是鼓励直接使用ThreadPoolExecutor类的构造方法,这样的处理方式主要是为了让使用线程池的同学更加清晰的明确线程池的各个参数含义,及其运行机制,从而规避系统资源被耗尽的风险。
Executors工具类返回线程池对象的弊端如下:
(1)newFixedThreadPool 和 newSingleThreadExecutor:允许请求的任务队列长度为Integer.MAX_ VALUE,可能会堆积大量的请求,从而导致OOM
(2)newCachedThreadPool 和 newScheduledThreadPool:允许创建的线程池线程数量为Integer.MAX_ VALUE,可能会无限制的创建大量线程,从而导致OOM
3.2 如何合理设置线程池参数
线程池的构造参数有7个,但是最核心的是3个:CorePoolSize、MaximumPoolSize、BlockingQueue,它们最大程度地决定了线程池中的任务分配和线程分配策略。如何合理的设置线程池参数,其实从一定程度上就是设置这三个参数。
3.2.1 理论计算方式
《Java高并发程序设计实战》一书中给出了这样的理论化计算公式:
简单算法:原则:线程等待时间所占的比例越高,需要设置越多线程。线程CPU时间所占比例越高,需要设置越少线程。
(1)CPU密集型任务:应该配置尽可能小的线程数,比如配置 Nthread = Ncpu + 1。
通常来说Nthread = Ncpu时,能够让CPU始终处于忙碌之中;多加一个线程能够保证,当线程池中其中一个线程由于特殊原因被中断,这个线程能够补上,从而使CPU继续保持忙碌。
(2)IO密集型任务:由于IO密集型任务线程并不是一直在执行任务,则应该尽可能多的配置线程,比如配置 Nthread = Ncpu * 2。
5 参考资料
(1)《Java并发编程的艺术》、《Java高并发程序设计实战》