目录
- 前言
- 一、Java 线程池的出现
- 二、Java 线程池的基础接口
- Executor 与 ExecutorService
- 三、Java 线程池的基础实现
- AbstractExecutorService
- ThreadPoolExecutor
- 1. 线程池的状态 —— 位运算
- 2. 生产消费模型 —— BlockingQueue(阻塞队列)
- 3. 任务执行者 —— Worker
- Worker 是什么
- Executor 如何管理 Worker
- 4. 执行函数 —— execute
- 5. 饱和策略 —— handler
- 四、为什么要使用线程池
前言
在上篇整理了 Java 线程与任务的概念,这篇说一说 Java 的线程池,在我们更加了解线程池的同时,也增加一些设计思路。
一、Java 线程池的出现
如果现在我们需要对并发的场景做性能优化,我们该从哪方面入手呢?显而易见,最简单的一个角度:节省线程创建与销毁的开销。
无论是IO密集应用还是计算密集应用,线程的管理都是需要成本的,所以从计算机的角度出发,我们还应该 限制线程的数量。创建线程的目的是使应用拥有更好的性能,而如果悬挂的线程过多,线程管理成本过高,应用的性能同样会下降。
对于以上两点需求,线程池就诞生了。
首先给大家看一下本文接口与实现类的关系:
二、Java 线程池的基础接口
Executor 与 ExecutorService
这是两个线程池的基础接口,ExecutorService 继承了 Executor。一般情况下,运用 ExecutorService 的实现比较多,因为拥有更完整的规范。先看一下这两个接口:
public interface Executor {
void execute(Runnable command);
}
与 Runnable 接口类似,定义非常简单,将任务实现放入执行器,执行(execute)任务。
public interface ExecutorService extends Executor {
//关闭控制类函数
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
//运行类函数
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
//批量运行类函数
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
在这里,我们可以看到线程池的雏形,为了更好的管理线程,实现线程池所需要的基本功能。
虽然 ExecutorService 的规范比 Executor 多了一些,但是总结无非就是三类:
- 关闭等控制类函数(池级别)
- 运行类函数(线程级别)
- 批量运行类型函数(第二种的扩展)
这里我们可以留意一下这两个接口:
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
感觉是不是很熟悉,这里先按下不表,下一节就能知道这里为什么要这样设计。
三、Java 线程池的基础实现
AbstractExecutorService
这个抽象类阶段性的实现了 ExecutorService 接口,在这个类中,明确了一些实现线程池的思路与步骤。
首先来看一下任务的基础实现与单任务的执行:
- 线程池的基础任务类型为 FutureTask
- 提交任务函数需要有2个步骤:新建任务对象 -> 执行器执行任务。
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
至此,我们理解了 submit 函数看起来为什么那么眼熟,是因为秉承着和 FutureTask 一样的逻辑,所以也提供了两个整齐的接口。
下面我们来看一下批量运行的处理方法,invokeAny 涉及到线程管理的调度,这里先按下不表,我们先看 invokeAll 中的实现:
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException {
if (tasks == null)
throw new NullPointerException();
ArrayList<Future<T>> futures = new ArrayList<>(tasks.size());
try {
for (Callable<T> t : tasks) {
RunnableFuture<T> f = newTaskFor(t);
futures.add(f);
execute(f);
}
for (int i = 0, size = futures.size(); i < size; i++) {
Future<T> f = futures.get(i);
if (!f.isDone()) {
try { f.get(); }
catch (CancellationException | ExecutionException ignore) {}
}
}
return futures;
} catch (Throwable t) {
cancelAll(futures);
throw t;
}
}
可以看到,取值是对整个 list 的一个遍历处理,因为是按 list 顺序 get 结果,所以其完成时间由 最慢任务决定。
到此为止,我们已经对线程池有了初步的认知,下面来看一下 Java 是怎么实现一个完整线程池的。
ThreadPoolExecutor
首先,我们可以通过构造方法来看一看,线程池的初始化最少需要什么元素。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
参数说明:
- corePoolSize:线程池核心线程数。加入一个任务时,如果当前线程数(空闲+非空闲)小于 corePoolSize,即便当前有空闲的线程,也会创建新的线程来执行任务。如果当前线程数(空闲+非空闲)等于 corePoolSize,则不再重新创建线程。
- maximumPoolSize:线程池最大线程数。如果阻塞队列已满,并且当前线程数(空闲+非空闲)小于等于 maximumPoolSize ,就会创建新的线程来执行任务。
- keepAliveTime:如果当前线程数(空闲+非空闲)大于 corePoolSize ,并且空闲时间大于 keepAliveTime的话,就会将这些空闲线程销毁,释放资源。
- unit:时间的单位。
- workQueue:阻塞队列的实例。
- ThreadFactory : 线程工厂类,一般使用默认工厂。
- RejectedExecutionHandler:饱和策略。在任务过多时,对任务的处理策略。
1. 线程池的状态 —— 位运算
为了提升运行的效率,减少开销,我们在记录一些属性值的时候,会用到 bit 位的操作,即位运算。(比如算法课上要用哈夫曼树实现压缩,就需要 bit 位记录源文件的各种属性数据)。
如果不清楚位运算等概念的同学可以移步:
java中的 位运算 和 移位运算详解原码、反码、补码知识详细讲解
我们来看一下,ThreadPollExecutor 是如何运用一个 int 来记录两项属性的:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//能存放 32-3 位的活跃线程数量。
private static final int COUNT_BITS = Integer.SIZE - 3;
//
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits(3 bits free)
// 在高位存储线程池状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~COUNT_MASK; }
private static int workerCountOf(int c) { return c & COUNT_MASK; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
2. 生产消费模型 —— BlockingQueue(阻塞队列)
在 ThreadPoolExecutor 中,管理保存任务的数据结构就是阻塞队列。其中,生产的方式用的是 offer ,消费的方式是 poll 与 take。
public interface BlockingQueue<E> extends Queue<E> {
//……………………
boolean offer(E e);
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;
//如果取出元素后,队列为空,一直阻塞等待。
E take() throws InterruptedException;
//如果取出元素后,队列为空,则阻塞超时后返回null。
E poll(long timeout, TimeUnit unit)
throws InterruptedException;
//……………………
}
接口规范中,很容易看到 poll 与 take 的区别。看一下在线程池中的应用:
public void execute(Runnable command) {
//……………………
//向队列生产任务
if (isRunning(c) && workQueue.offer(command)) {
//……………………
}
//……………………
}
private Runnable getTask() {
//……………………
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
try {
//依据元素是否是具备时间(timed)属性,而选择消费任务的方式。
Runnable r = timed ?
//如果queue is null,即没有可执行任务,则阻塞超时后 return null。
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
Tips:
依据元素是否是具备时间(timed)属性,而选择消费任务的方式。
如果是 poll 消费方式,则元素具备超时属性(具体可以看BlockingQueue的源码),会在取值处阻塞,直到达到超时(空闲)时间,返回null,在 runWorker() 函数中,调用销毁线程方法 processWorkerExit ,从而达到:线程数 > corePoolSize 时,空闲线程按时回收。
而 take 消费方式,则会一直阻塞等待,对于 getTask() 而言,总有返回值,即不会销毁线程。从而达到:线程数 <= corePoolSize 时,空闲线程不回收。
3. 任务执行者 —— Worker
在 ThreadPoolExecutor 中,有一个 Worker 的内部类。简单来说就是任务(runnable的实现 —— task)的容器,可以粗暴的理解为一个线程,但是不完全相同。
Worker 是什么
实际上 Worker 是一个任务容器,并且构造方法非常简单:
private final class Worker extends AbstractQueuedSynchronizer
implements Runnable
{
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
}
创建一个新的线程对象,并向其中添加一个任务。
Executor 如何管理 Worker
首先 Executor 是通过一个 set 来存储 Worker 的,确保每个 worker 的唯一性。
private final HashSet<Worker> workers = new HashSet<>();
其次,对于线程池而言,最重要的就是 addWorker 和 runWorker 了。这里就不贴源码了,简单说一下这两个函数的区别与用途。
addWorker() 函数是 真正创建 Worker 实例并开启线程的地方 ,因为其中找到了 thread.start() 函数。稍微检索一下,我们就可以看到 execute() 函数里面调用了 addWorker,所以可以得知:线程池如果要执行任务,就需要调用execute() 函数。
runWorker() 函数实际上就是 Worker 的 run,(补充说明)。
4. 执行函数 —— execute
这里的代码,揭示了线程池线程池在线程数不同情况下的处理策略。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//池中现有线程个数(空闲+非空闲) < 核心线程数
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//池中现有线程个数(空闲+非空闲) >= 核心线程数,或线程创建失败,放入阻塞队列。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//阻塞队列满,新建线程执行任务知道 max,如果创建失败或任务超过max,则启用饱和策略——中断。
else if (!addWorker(command, false))
reject(command);
}
5. 饱和策略 —— handler
饱和策略是用于处理在线程异常与线程数已满的情况下,处理当前任务的处理方式。大家可以稍作了解。
- AbortPolicy: 中止执行提交的任务,抛出 RejectedExecutionException 异常;这是线程池中默认的策略。
//默认饱和策略
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy();
//实现在池子中的内部类
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
}
- CallerRunsPolicy:用调用者所在的线程来执行任务,即在调用者视角,此任务串行在调用者的主线程中。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
- DiscardPolicy:不处理直接丢弃掉任务(执行空函数);
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
- DiscardOldestPolicy:丢弃阻塞队列中头部任务(即存放最久的任务),执行当前任务。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
Tips:
实际上,Java 的饱和策略算是抛砖引玉,在业务场景中,可以针对特定的业务功能,自定义饱和策略。这才是关键。
四、为什么要使用线程池
经过以上了解,我们可以总结出线程池的价值:
- 降低资源消耗。通过复用已存在的线程,降低创建和销毁线程的资源消耗;
- 提高线程的可管理性。线程是稀缺资源,不能无限增生,过多的线程也会给计算机造成负担。