接上一篇《Java并发系列(10)——FutureTask 和 CompletionService》
文章目录
9 线程池
9.1 JDK 线程池
JDK 线程池的实现大致如上图。
Executor 接口方法:
- execute(Runnable):提交一个 Runnable 任务,由 Executor 决定怎么执行;
ExecutorService 接口方法:
- submit 系列:可以提交 Callable 任务,可以有返回值;
- invoke 系列:批量提交任务,并等待任务完成;
- ExecutorService 状态相关方法:
- shutdown;
- awaitTermination;
- isShutdown;
- isTerminated;
ScheduledExecutorService 接口方法:
- schedule 系列:可以做定时任务。
值得一提的是,Executor,ExecutorService,AbstractExecutorService,ScheduledExecutorService 这四个其实不算是线程池。它们仅仅是 Executor,没有规定一定要用线程池实现。
ForkJoinPool,ThreadPoolExecutor,ScheduledThreadPoolExecutor 三个才算是线程池,它们是 ExecutorService 接口的线程池实现。
9.2 ThreadPoolExecutor
Executors 类里面的 newFixedThreadPool,newCachedThreadPool 返回的就是这个 ThreadPoolExecutor。
9.2.1 参数
一共 8 个,其中 7 个在构造方法里面。
9.2.1.1 corePoolSize,maximumPoolSize
核心线程数和最大线程数。这两个参数规定了线程池里面工作线程的数量。
正常是 corePoolSize 个工作线程(核心线程),最大可以有 maximumPoolSize 个工作线程(最大线程)。最大线程去掉核心线程的那些,可以理解为“备用线程”,意思是一般情况不使用,只有当并发量太大,核心线程扛不住的时候才会逐渐启动一些备用线程。
工作线程数量的变化可能会经历以下几个阶段:
- 起初,工作线程数从 1 开始慢慢增大,直到任务产生速度与任务消费速度达到平衡(假设还没有达到核心线程数);
- 当任务产生速度加快,工作线程数随之增大,假设达到核心线程数,仍然赶不上任务产生的速度,线程数不再增加,来不及处理的任务进入队列;
- 任务产生速度不降低,队列终究会被塞满,此时会迅速启动大量备用线程,任务消费速度是大于任务产生速度的,因为要将队列里的任务清空;
- 当队列里面任务清空后,线程数量逐渐减少,直到任务消费速度与任务产生速度达到平衡;
- 如果工作线程数达到最大仍然赶不上任务产生速度,任务就会被线程池拒绝。
9.2.1.2 keepAliveTime,timeUnit
定义线程空闲时的存活时间。到时间依然没有接到任务,则线程终止。
默认情况,当工作线程数大于核心线程数时,才会终止,低于核心线程数时会一直等待不会终止,除非将 allowCoreThreadTimeOut 参数置为 true。
9.2.1.3 workQueue
工作队列,是一个并发阻塞队列,可以有界也可以无界。
来不及处理的任务暂时放在工作队列中。
9.2.1.4 threadFactory
线程工厂,用于创建线程对象。
9.2.1.5 rejectExecutionHandler
当任务被拒绝时,如:
- 工作线程已达最大值,工作队列也放满;
- 线程池非 running 状态;
则会将被拒绝的任务交给这个 handler 来处理。
9.2.1.6 allowCoreThreadTimeOut
允许核心线程获取任务超时,自动终止。
9.2.2 线程池状态
共五种状态,并且这五种状态存在递进关系。
9.2.2.1 RUNNING
线程池正常工作,初始态。
9.2.2.2 SHUTDOWN
在此状态下:
- 不接受新任务;
- 正在处理的任务继续处理;
- interrupt 空闲线程;
- 工作队列里的任务也会被处理。
在 shutdown 状态之后,工作线程数会逐渐减少,直到 0,线程池 terminated。
9.2.2.3 STOP
此状态:
- 不接受新任务;
- interrupt 所有线程,即:
- 等待任务的空闲线程被打断不再等待,线程终止;
- 正在工作的线程如果不响应 interrupt 会继续工作,如果响应 interrupt,比如任务中存在 wait/sleep 等,则可能被打断抛出 InterruptedException;
- 工作队列里不会有任务,因为已经被清空了。
9.2.2.4 TIDYING
terminated 之前的过度状态。
stop 状态下,工作队列已经是空的了,只要等所有工作线程把正在处理的任务处理掉,就可以进入 terminated 状态。
而在进入 terminated 状态之前,会先进入 tidying 状态,然后调用一个 hook 方法(terminated 方法),hook 方法调用后就会进入 terminated 状态。
简单来说,tidying 状态与 terminated 状态之间相差一个 hook 方法的调用。
此状态下:
- 不接受新任务;
- 没有工作线程;
- 工作队列没有任务。
9.2.2.5 TERMINATED
同 tidying 状态,区别在于 hook 方法已经被调用过了。
9.2.2.6 状态切换
初始态:running;
调用 shutdown() 方法:running -> shutdown;
调用 shutdownNow() 方法:running/shutdown -> stop;
tidying 和 terminated 状态没有方法调用,条件达成自然进入:
- 进入 tidying:在 shutdown/stop 之后,没有工作线程,工作队列没有任务;
- 进入 terminated:在 tidying 之后,terminated 方法调用完成。
同时,五种状态从 running 到 terminated 依次递进,只进不退。
9.2.3 核心功能
9.2.3.1 execute
提交一个 Runnable 类型的任务,没有返回值。
有几个注意点:
- 只有 RUNNING 状态才可以接受新任务;
- 接到新任务时:
- 优先,启动核心线程执行(即使核心线程空闲);
- 其次,加入队列,等待有线程空闲时执行;
- 再次,启动备用线程执行;
- 最后,拒绝任务,并交给一个 handler 处理,可能直接抛出 RejectedExecutionException。
9.2.3.2 submit
submit 系列有三个方法,都是返回 Future,可以从 Future 里面拿到执行结果。
9.2.3.3 invoke
invoke 系列有四个方法:
- invokeAll(Collection),一次性提交所有的 Callable,会阻塞等待所有 Callable 被执行完成;
- invokeAll(Collection, long, TimeUnit),一次性提交所有的 Callable,会阻塞等待所有 Callable 被执行完成或者超时,超时之后所有未提交的、已提交未被执行的、正在执行未执行完的任务都会被 cancel(true);
- invokeAny(Collection),一次性提交所有的 Callable,会阻塞直到出现第一个正常执行完成的任务,此后所有未提交的、已提交未被执行的、正在执行未执行完的任务都会被 cancel(true);
- invokeAny(Collection, long, TimeUnit),一次性提交所有的 Callable,会阻塞直到出现第一个正常执行完成的任务或超时,此后所有未提交的、已提交未被执行的、正在执行未执行完的任务都会被 cancel(true)。
TIPS:
- Callable 被执行完成包括三种情况:正常执行完成,抛出异常,被取消。
- cancel(true) 是一种会 interrupt 正在执行的任务的取消操作,与 cancel(false) 有所区别。
- 在第 8 章有更为详尽的阐述。
9.2.3.4 shutdown
做三件事:
- 线程池状态改成 SHUTDOWN;
- interrupt 所有没有在执行任务的线程;
- 调用 hook 方法 onShutdown()。
9.2.3.5 shutdownNow
做三件事:
- 线程池状态改成 STOP;
- interrupt 所有线程,包括正在执行任务的线程;
- 队列里的任务清空。
9.2.3.6 isShutdown
并不仅仅是 SHUTDOWN 状态,还包括后面的 STOP,TIDYING,TERMINATED 状态。
9.2.3.7 isTerminated
TERMINATED 状态。
9.2.3.9 awaitTermination
阻塞方法,等待线程池 TERMINATED,或者超时。
9.2.4 手写 ThreadPoolExecutor
了解了 ThreadPoolExecutor 的核心功能之后,我们来自己实现一个,这样会理解得更加深刻。
9.2.4.1 构造方法
ThreadPoolExecutor 的构造方法有 7 个参数,这里我们也把这 7 个参数都用上。
public class LvjcThreadPoolExecutor implements ExecutorService {
private volatile int corePoolSize;
private volatile int maximumPoolSize;
private volatile long keepAliveTime;
private final BlockingQueue<Runnable> workQueue;
private final ThreadFactory threadFactory;
private final LvjcRejectedExecutionHandler rejectedExecutionHandler;
private final HashSet<Worker> workers;
public LvjcThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
LvjcRejectedExecutionHandler handler) {
//*1.规定核心线程数最小为 1(因为如果允许核心线程数为 0 要处理一种特例)
this.corePoolSize = corePoolSize > 0 ? corePoolSize : 1;
//2.规定最大线程数必须 >= 核心线程数
this.maximumPoolSize = Math.max(maximumPoolSize, corePoolSize);
//3.线程最大空闲时间,不管怎么传参,统一转为 ns 单位,便于处理
this.keepAliveTime = keepAliveTime <= 0 || unit == null
? TimeUnit.SECONDS.toNanos(60)
: unit.toNanos(keepAliveTime);
//4.工作队列,设置一个默认的无界阻塞队列
this.workQueue = workQueue == null ? new LinkedBlockingQueue<>() : workQueue;
//5.提供一个默认的线程工厂,自己手写
this.threadFactory = threadFactory == null ? new LvjcDefaultThreadFactory() : threadFactory;
//6.提供一个默认的拒绝处理器,自己手写
this.rejectedExecutionHandler = handler == null ? new DefaultRejectExecutionHandler() : handler;
//7.new 一个容器出来保存工作线程
//要考虑并发安全,但这里不用线程安全容器,因为后面我们会加锁
this.workers = new HashSet<>();
}
}
9.2.4.2 实现 ThreadFactory
构造方法里面,我们提供一个默认的线程工厂,实现如下:
package per.lvjc.concurrent.pool;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 默认线程工厂,基本照搬 {@link java.util.concurrent.Executors.DefaultThreadFactory}
*/
public class LvjcDefaultThreadFactory implements ThreadFactory {
private AtomicInteger threadNum;
private String threadNamePrefix;
private ThreadGroup threadGroup;
public LvjcDefaultThreadFactory() {
this.threadGroup = Thread.currentThread().getThreadGroup();
this.threadNamePrefix = "lvjc-pool-thread-";
this.threadNum = new AtomicInteger(1);
}
//实现 ThreadFactory 接口唯一的一个接口方法
@Override
public Thread newThread(Runnable r) {
//把入参 Runnable new 一个 Thread 出去
return new Thread(threadGroup, r, threadNamePrefix + threadNum.getAndIncrement());
}
}
线程工厂的实现比较简单,玩不出什么花样来,JDK 基本上也是这么写的。
主要目的是统一管理线程池里面的线程。
基本需求就是每个线程池都设置一个具有辨识度的名称,这样出了问题一看日志根据线程名称就可以快速定位到属于哪块业务。
9.2.4.3 实现 RejectExecutionHandler
package per.lvjc.concurrent.pool;
/**
* 当任务被拒绝时调用。
* 重新定义这个接口,因为我们实现的 LvjcThreadPoolExecutor 跟 jdk 的 ThreadPoolExecutor
* 不是一个类,没有办法传参。
*/
public interface LvjcRejectedExecutionHandler {
void rejectedExecution(Runnable r, LvjcThreadPoolExecutor executor);
}
package per.lvjc.concurrent.pool;
public class DefaultRejectExecutionHandler implements LvjcRejectedExecutionHandler {
//实现唯一的接口方法
@Override
public void rejectedExecution(Runnable r, LvjcThreadPoolExecutor executor) {
//拒绝就算了,我们什么都不干
}
}
9.2.4.4 实现任务消费
因为线程池里的线程是可以复用的,如果直接用 Thread,那么跑一次线程就结束了,没办法复用,所以我们要做一层封装,与前面讲到的 FutureTask 封装 Callable 类似。
怎么实现跑完一个任务的 run 方法不结束线程呢?
基本思路是外面套一层死循环,来一个任务就拿出来处理,没有任务就阻塞等着即可,就像消息队列的消费者。
参照 FutureTask 的实现思路,实现 Runnable 接口,在 run 方法里面做一些特殊处理。
定义一个 Worker 类代表一个工作线程,实现 run 方法:
/**
* 表示线程池里的一个工作线程
*/
public class Worker implements Runnable {
@Override
public void run() {
try {
Runnable task = firstTask;
firstTask = null;
while (task != null || (task = getTaskFromWorkQueue()) != null) {
//1.执行任务前,把当前线程置为非空闲状态
//其它线程如果想并发干点事情,比如 shutdown,
//看到这个状态,该怎么处理它就有数了。
idle = false;
//2.执行工作任务
try {
task.run();
} finally {
task = null;
}
//3.任务执行完成后,再把当前线程改回空闲状态
idle = true;
}
} finally {
//4.线程结束前的收尾工作
//4.1.清除当前工作线程
executor.getWorkers().remove(this);
//4.2.工作线程数量 -1
executor.decrementWorkerCount();
//4.3.尝试使线程池进入 terminated 状态