Java并发与多线程-详解线程池

Java并发与多线程-详解线程池

什么是线程池?

在此,我们参考一下百科的定义:

线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。

线程池的作用?

了解线程池作用之前,我们先来看一下,CPU的上下文切换,有多费时间:

拿一台主频2.6G的电脑来说,每秒可以执行 2.6*10^9个指令,每个指令只需要 0.38ns,为了方便理解,我们把这个时间单位,换算成人类世界的1秒
一次 CPU 上下文切换(系统调用)需要大约 1500ns,也就是 1.5us(这个数字采用的是单核 CPU 线程平均时间),换算成人类时间大约是 65分钟,嗯,也就是一个小时。我们也知道上下文切换是很耗时的行为,毕竟每次浪费一个小时,也很让人有罪恶感的。上下文切换更恐怖的事情在于,这段时间里 CPU没有做任何有用的计算,只是切换了两个不同进程的寄存器和内存状态;而且这个过程还破坏了缓存,让后续的计算更加耗时。
备注:上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程或线程切换到另一个进程或线程。

如果想让程序运行的更快,我们需要减少CPU上下文切换的次数。
所以,需要避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。使用线程池,可以有效避免类似的问题。线程池解决的核心问题就是资源管理问题。对使用到的线程进行统一管理,避免创建不必要的线程。

使用线程池,还可以带来如下好处:

  • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
    既然线程池有这么多好处,那我们如何使用呢?
    首先,要用好线程池,需要知道线程池的生命周期以及线程池的主要参数配置。

线程池生命周期

ThreadPoolExecutor的运行状态有5种:

  • RUNNING
  • SHUTDOWN
  • STOP
  • TIDYING
  • TERMINATED

源码定义如下:
java.util.concurrent.ThreadPoolExecutor.ctl
文档注释摘录

The runState provides the main lifecycle control, taking on values:
  RUNNING:  Accept new tasks and process queued tasks
(译:接受新任务并处理排队的任务)
SHUTDOWN: Don't accept new tasks, but process queued tasks
(译:不接受新任务,但处理已排队的任务)
STOP:     Don't accept new tasks, don't process queued tasks,
            and interrupt in-progress tasks
(译:不接受新任务,不处理排队的任务,并中断正在进行的任务)
TIDYING:  All tasks have terminated, workerCount is zero,
            the thread transitioning to state TIDYING
            will run the terminated() hook method
(译:所有任务已终止,workerCount为零,线程转换到 TIDYING 状态,会运行terminated()钩子方法)
TERMINATED: terminated() has completed
(译:terminated()方法执行完毕)

在这里插入图片描述
图:线程池5种状态-流程图

状态 是否接受新任务 排队任务的处理 其他
RUNNING 接受新任务 处理排队的任务 正常运行
SHUTDOWN 不接受新任务 处理排队的任务 不接受新任务,但处理已排队的任务
STOP 不接受新任务 不处理排队的任务 不接受新任务,不处理排队的任务,并中断正在进行的任务
TIDYING 不接受新任务 - 所有任务已终止
TERMINATED 不接受新任务 - terminated()方法执行完毕

详解线程池参数

在这里插入图片描述
图:线程池参数列表.png

  • 第一个参数设置核心线程数。默认情况下核心线程会一直存活。
  • 第二个参数设置最大线程数。决定线程池最多可以创建的多少线程。
  • 第三个参数和第四个参数用来设置线程空闲时间,和空闲时间的单位,当线程闲置超过空闲时间就会被销毁。可以通过 allowCoreThreadTimeOut 方法来允许核心线程被回收。
  • 第五个参数设置缓冲队列,上图中左下方的三个队列是设置线程池时常使用的缓冲队列。其中 ArrayBlockingQueue 是一个有界队列,就是指队列有最大容量限制。LinkedBlockingQueue 是无界队列,就是队列不限制容量。最后一个是 SynchronousQueue,是一个同步队列,内部没有缓冲区。
  • 第六个参数设置线程池工厂方法,线程工厂用来创建新线程,可以用来对线程的一些属性进行定制,例如线程的 group、线程名、优先级等。一般使用默认工厂类即可。
  • 第七个参数设置线程池满时的拒绝策略。如上图右下方所示有四种策略,Abort 策略在线程池满后,提交新任务时会抛出 RejectedExecutionException,这个也是默认的拒绝策略。Discard 策略会在提交失败时对任务直接进行丢弃。CallerRuns 策略会在提交失败时,由提交任务的线程直接执行提交的任务。DiscardOldest 策略会丢弃最早提交的任务。
    接着,我们需要了解一下线程池的执行流程:

线程池执行流程

在这里插入图片描述
图:线程池任务执行流程.png

  1. 向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数,如果不大于,就创建一个核心线程来执行任务。
  2. 如果大于核心线程数,就会判断缓冲队列是否满了,如果没有满,则放入队列,等待线程空闲时执行任务。
  3. 如果队列已经满了,则判断是否达到了线程池设置的最大线程数,如果没有达到,就创建新线程来执行任务。
  4. 如果已经达到了最大线程数,则执行指定的拒绝策略。
    这里需要注意队列的判断与最大线程数判断的顺序,不要搞反。

了解了理论,接下来,我们通过一个实战,来实地观察一下线程池任务的执行流程。

实战源码

  • 程序目的:观察ThreadPoolExecutor的执行流程
    包含如下组成部分:
  • 一个监听线程池状态的类:MyMonitorThread,每1秒输出一次线程池状态。
  • 一个RejectedExecutionHandlerImpl,用来执行拒绝策略,会打印那些任务被拒绝了。
  • 一个工作线程定义:WorkerThread 会模拟2秒的任务执行耗时。
  • 主程序:WorkerPoolApplication 用来执行程序

MyMonitorThread.java

import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * 监控线程池状态(每隔N秒,输出一次线程池的状态)
 * 分别会打印如下信息:
 * </pre>
 * created at 2019-05-29 14:12
 * @author lerry
 */
@Slf4j
public class MyMonitorThread implements Runnable {

	/**
	 * 持有被监控的线程池对象
	 */
	private ThreadPoolExecutor threadPoolExecutor;

	/**
	 * 每隔多久执行一次
	 */
	private int delay;

	/**
	 * 如果为false、则关闭监听
	 */
	private boolean isRun = true;

	public MyMonitorThread(ThreadPoolExecutor threadPoolExecutor, int delay) {
		this.threadPoolExecutor = threadPoolExecutor;
		this.delay = delay;
	}

	public void shutDown() {
		this.isRun = false;
	}

	@Override
	public void run() {
		while (isRun) {
			// 获取等待队列
			BlockingQueue<Runnable> queue = this.threadPoolExecutor.getQueue();
			// 等待队列转为仅存储 指令名称 的List
			List<String> queueList = queue.stream().map(r -> {
				if (r instanceof WorkerThread) {
					return ((WorkerThread) r).getCommand();
				}
				else {
					return "";
				}
			}).collect(Collectors.toList());
			// 日志记录线程池状态
			/*
			 * poolSize:池中的当前线程数
			 * corePoolSize: 核心线程数
			 * Active:当前主动执行任务的近似线程数量
			 * Completed:已完成执行的任务的总数(由于任务和线程的状态在计算过程中可能会动态变化,因此返回的值仅是一个近似值,而在连续的调用中不会降低。)
            * TaskCount:计划执行的任务数
			 * queue:缓冲队列
			 * isShutdown:如果此执行程序已关闭,则返回true
			 * isTerminated:观察线程池是否终结
			 */
			log.info("poolSize/corePoolSize [{}/{}] Active: {}, Completed: {}, Task: {}, queue:{},isShutdown: {}, isTerminated: {}",
					this.threadPoolExecutor.getPoolSize(),
					this.threadPoolExecutor.getCorePoolSize(),
					this.threadPoolExecutor.getActiveCount(),
					this.threadPoolExecutor.getCompletedTaskCount(),
					this.threadPoolExecutor.getTaskCount(),
					queueList,
					this.threadPoolExecutor.isShutdown(),
					this.threadPoolExecutor.isTerminated());
			// 间隔N秒输出一次线程池状态
			try {
				Thread.sleep(delay * 1000);
			}
			catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

RejectedExecutionHandlerImpl.java

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

import lombok.extern.slf4j.Slf4j;

/**
 * 还可以创建自己的 RejectedExecutionHandler 实现来处理没有放在工作队列里的任务。
 * rejected:拒绝的
 * created at 2019-05-29 14:09
 * @author lerry
 */
@Slf4j
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
	@Override
	public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
		WorkerThread workerThread = null;
		if (r instanceof WorkerThread) {
			workerThread = (WorkerThread) r;
		}
		log.info("[{}] is rejected(被拒绝/驳回)", workerThread.getCommand());
	}
}

WorkerThread.java

/**
 * 工作线程
 * created at 2019-05-29 11:25
 * @author lerry
 */
@Slf4j
public class WorkerThread implements Runnable {

	/**
	 * 执行的任务编号
	 */
	private String command;

	/**
	 * 模拟工作线程的执行耗时(单位:秒)
	 */
	private int executeDuration;

	public WorkerThread(String command) {
		this.command = command;
	}

	public WorkerThread(String command, int executeDuration) {
		this.command = command;
		this.executeDuration = executeDuration;
	}

	@Override
	public void run() {
		log.info("{}  Start. Command = {}", Thread.currentThread().getName(), command);
		processCommand();
		log.info("{}  End. Command = {}", Thread.currentThread().getName(), command);
	}

	/**
	 * 模拟任务执行耗时
	 */
	private void processCommand() {
		try {
			Thread.sleep(executeDuration * 1000);
		}
		catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public String getCommand() {
		return command;
	}
}

WorkerPoolApplication.java

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import lombok.extern.slf4j.Slf4j;

/**
 * <pre>
 * 程序目的:观察ThreadPoolExecutor的执行流程
 * 包含:一个监听线程池状态的类:MyMonitorThread,每1秒输出一次线程池状态。
 * 一个RejectedExecutionHandlerImpl,用来执行拒绝策略,会打印那些任务被拒绝了。
 * 一个工作线程定义:WorkerThread 会模拟2秒的执行时间。
 * 主程序:WorkerPoolApplication 用来执行程序
 * 请注意:在初始化 ThreadPoolExecutor 时,核心线程数大小设为2、最大线程数设为5、缓冲队列大小设为2。
 * 所以,一共提交10个任务的情况下,前2个任务,线程池会创建核心线程、并执行该任务;
 * 第3——4个任务,会进入等待队列,队列满了之后,
 * 第5——7个任务,会继续创建线程、执行该任务;
 * 最后的第8、9、10个任务,会执行拒绝策略,交由RejectedExecutionHandlerImpl 处理,被rejected。
 * </pre>
 * created at 2019-05-29 14:16
 * @author lerry
 */
@Slf4j
public class WorkerPoolApplication {

	public static void main(String[] args) throws InterruptedException {
		RejectedExecutionHandlerImpl rejectedExecutionHandler = new RejectedExecutionHandlerImpl();

		ThreadFactory threadFactory = Executors.defaultThreadFactory();

		// 线程池
		/*
		 *  corePoolSize:核心线程数 默认情况下核心线程会一直存活
		 *	maximumPoolSize:最大线程数 决定线程池最多可以创建的多少线程
		 *	keepAliveTime、unit:线程空闲时间,和空闲时间的单位 当线程闲置超过空闲时间就会被销毁
		 *	workQueue:缓冲队列
		 *  threadFactory:设置线程池工厂方法,线程工厂用来创建新线程,可以用来对线程的一些属性进行定制,例如线程的 group、线程名、优先级等
		 *  RejectedExecutionHandler: 设置线程池满时的拒绝策略
		 */
		ThreadPoolExecutor executorPool = new ThreadPoolExecutor(
				2,
				5,
				10,
				TimeUnit.SECONDS,
				new ArrayBlockingQueue<Runnable>(2),
				threadFactory,
				rejectedExecutionHandler
		);

		// 启动监控线程
		MyMonitorThread monitor = new MyMonitorThread(executorPool, 1);
		new Thread(monitor).start();

		for (int i = 1; i <= 10; i++) {
			executorPool.execute(new WorkerThread("cmd" + i, 2));
		}

		log.info("关闭线程池");
		executorPool.shutdown();

		for (; ; ) {
			// 线程池终止后,关闭监控线程
			if (executorPool.isTerminated()) {
				// 等待监视器打印线程池 isTerminated: true 的状态
				Thread.sleep(1_000);
				monitor.shutDown();
				break;
			}
		}// end for
	}
}

执行结果

2020-07-05 11:00:10.287 [pool-1-thread-3] INFO  WorkerThread - pool-1-thread-3  Start. Command = cmd5
2020-07-05 11:00:10.287 [pool-1-thread-4] INFO  WorkerThread - pool-1-thread-4  Start. Command = cmd6
2020-07-05 11:00:10.287 [pool-1-thread-5] INFO  WorkerThread - pool-1-thread-5  Start. Command = cmd7
2020-07-05 11:00:10.287 [pool-1-thread-2] INFO  WorkerThread - pool-1-thread-2  Start. Command = cmd2
2020-07-05 11:00:10.287 [pool-1-thread-1] INFO  WorkerThread - pool-1-thread-1  Start. Command = cmd1
2020-07-05 11:00:10.287 [main    ] INFO  RejectedExecutionHandlerImpl - [cmd8] is rejected(被拒绝/驳回)
2020-07-05 11:00:10.294 [main    ] INFO  RejectedExecutionHandlerImpl - [cmd9] is rejected(被拒绝/驳回)
2020-07-05 11:00:10.294 [main    ] INFO  RejectedExecutionHandlerImpl - [cmd10] is rejected(被拒绝/驳回)
2020-07-05 11:00:10.294 [main    ] INFO  WorkerPoolApplication - 关闭线程池
2020-07-05 11:00:10.359 [Thread-0] INFO  MyMonitorThread - poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false
2020-07-05 11:00:11.364 [Thread-0] INFO  MyMonitorThread - poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false
2020-07-05 11:00:12.294 [pool-1-thread-5] INFO  WorkerThread - pool-1-thread-5  End. Command = cmd7
2020-07-05 11:00:12.294 [pool-1-thread-4] INFO  WorkerThread - pool-1-thread-4  End. Command = cmd6
2020-07-05 11:00:12.294 [pool-1-thread-5] INFO  WorkerThread - pool-1-thread-5  Start. Command = cmd3
2020-07-05 11:00:12.294 [pool-1-thread-2] INFO  WorkerThread - pool-1-thread-2  End. Command = cmd2
2020-07-05 11:00:12.294 [pool-1-thread-3] INFO  WorkerThread - pool-1-thread-3  End. Command = cmd5
2020-07-05 11:00:12.294 [pool-1-thread-1] INFO  WorkerThread - pool-1-thread-1  End. Command = cmd1
2020-07-05 11:00:12.294 [pool-1-thread-4] INFO  WorkerThread - pool-1-thread-4  Start. Command = cmd4
2020-07-05 11:00:12.366 [Thread-0] INFO  MyMonitorThread - poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false
2020-07-05 11:00:13.370 [Thread-0] INFO  MyMonitorThread - poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false
2020-07-05 11:00:14.297 [pool-1-thread-4] INFO  WorkerThread - pool-1-thread-4  End. Command = cmd4
2020-07-05 11:00:14.297 [pool-1-thread-5] INFO  WorkerThread - pool-1-thread-5  End. Command = cmd3
2020-07-05 11:00:14.374 [Thread-0] INFO  MyMonitorThread - poolSize/corePoolSize [0/2] Active: 0, Completed: 7, Task: 7, queue:[],isShutdown: true, isTerminated: true

执行结果解读

首先,工作线程5、6、7和2、1被创建并启动,一共10个任务,但是线程池最大线程数设置的是5,等待队列大小设置的是2,剩下的三个线程(8、9、10),被执行拒绝策略。
这时,我们手动调用shutdown(),尝试关闭线程池。因为还有线程未执行完,等待队列中也有任务,所以线程池会等待事情全部处理好后,再关闭。
这时,通过监控日志,可以发现:

poolSize/corePoolSize [5/2] Active: 5, Completed: 0, Task: 7, queue:[cmd3, cmd4],isShutdown: true, isTerminated: false

当前池中线程数为5、计划执行的线程数为7,3、4号工作线程在队列中。
接着,7和6执行完毕,队列中的3号任务开始执行,2、5、1也相继执行完毕,队列中的4号任务开始执行。这时再次观察线程池状态:

poolSize/corePoolSize [2/2] Active: 2, Completed: 5, Task: 7, queue:[],isShutdown: true, isTerminated: false

可以看到,当前池中线程数为2、已完成执行的任务的总数为5,计划执行的线程数为7,队列为空。
继续,4号和3号线程执行完毕,最后查看线程池状态:

poolSize/corePoolSize [0/2] Active: 0, Completed: 7, Task: 7, queue:[],isShutdown: true, isTerminated: true

可以看到,当前池中线程数为0、已完成执行的任务的总数为7,计划执行的线程数为7,队列为空。线程池关闭。
我们发现、超过线程池核心线程数的、小于最大线程数的这部分线程,优先于等待队列中的任务执行。

目录结构

在这里插入图片描述
图:本文目录结构

参考资料

线程池_百度百科
让 CPU 告诉你硬盘和网络到底有多慢 | Cizixs Write Here
多线程上下文切换 - 五月的仓颉 - 博客园
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队

环境说明

  • java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

  • OS:macOS High Sierra 10.13.4
  • 日志:logback
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章