接上一篇《Java并发系列(10)——FutureTask 和 CompletionService》

文章目录

  • 9 线程池
    • 9.1 JDK 线程池
    • 9.2 ThreadPoolExecutor
      • 9.2.1 参数
        • 9.2.1.1 corePoolSize,maximumPoolSize
        • 9.2.1.2 keepAliveTime,timeUnit
        • 9.2.1.3 workQueue
        • 9.2.1.4 threadFactory
        • 9.2.1.5 rejectExecutionHandler
        • 9.2.1.6 allowCoreThreadTimeOut
      • 9.2.2 线程池状态
        • 9.2.2.1 RUNNING
        • 9.2.2.2 SHUTDOWN
        • 9.2.2.3 STOP
        • 9.2.2.4 TIDYING
        • 9.2.2.5 TERMINATED
        • 9.2.2.6 状态切换
      • 9.2.3 核心功能
        • 9.2.3.1 execute
        • 9.2.3.2 submit
        • 9.2.3.3 invoke
        • 9.2.3.4 shutdown
        • 9.2.3.5 shutdownNow
        • 9.2.3.6 isShutdown
        • 9.2.3.7 isTerminated
        • 9.2.3.9 awaitTermination
      • 9.2.4 手写 ThreadPoolExecutor
        • 9.2.4.1 构造方法
        • 9.2.4.2 实现 ThreadFactory
        • 9.2.4.3 实现 RejectExecutionHandler
        • 9.2.4.4 实现任务消费
        • 9.2.4.5 实现从队列获取任务
        • 9.2.4.6 实现提交任务
        • 9.2.4.7 实现 shutdown
        • 9.2.4.8 实现 shutdownNow
        • 9.2.4.9 实现 tryTerminate
        • 9.2.4.10 实现 awaitTermination
        • 9.2.4.11 实现 isShutdown 和 isTerminated
        • 9.2.4.12 实现 submit 提交任务
      • 9.2.5 JDK 的实现
        • 9.2.5.1 线程状态与线程数的处理
        • 9.2.5.2 corePoolSize 可为 0
        • 9.2.5.3 核心线程超时
        • 9.2.5.4 预创建核心线程
        • 9.2.5.5 invokeAll 方法
        • 9.2.5.6 invokeAny 方法
        • 9.2.5.7 钩子方法
        • 9.2.5.8 拒绝策略

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 接口唯一的一个接口方法@Overridepublic 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 {//实现唯一的接口方法@Overridepublic void rejectedExecution(Runnable r, LvjcThreadPoolExecutor executor) {//拒绝就算了,我们什么都不干}
}
9.2.4.4 实现任务消费

因为线程池里的线程是可以复用的,如果直接用 Thread,那么跑一次线程就结束了,没办法复用,所以我们要做一层封装,与前面讲到的 FutureTask 封装 Callable 类似。

怎么实现跑完一个任务的 run 方法不结束线程呢?

基本思路是外面套一层死循环,来一个任务就拿出来处理,没有任务就阻塞等着即可,就像消息队列的消费者。

参照 FutureTask 的实现思路,实现 Runnable 接口,在 run 方法里面做一些特殊处理。

定义一个 Worker 类代表一个工作线程,实现 run 方法:

/*** 表示线程池里的一个工作线程*/
public class Worker implements Runnable {@Overridepublic 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.工作线程数量 -1executor.decrementWorkerCount();//4.3.尝试使线程池进入 terminated 状态//因为 terminated 状态是线程池根据自身运行状况自动进入的,//所以需要有一些地方来触发线程池检查自身运行状况,看是否需要进入 terminated 状态,//这里是其中一个触发点executor.tryTerminate();}}
}

然后再补充定义 run 方法里面需要用到的成员变量:

  • idle:boolean 类型标记线程是否正在执行任务,因为可能多线程访问,所以需要申明 volatile;
  • firstTask:Runnable 类型,因为在当前工作线程数量少于核心线程数时,提交的任务不会进入队列,没有办法从队列里拿到,所以定义一个变量,当新建线程时直接把任务传进来;
  • executor:因为要调用线程池的一些方法,所以这里直接存一份线程池的引用,如果把 Worker 类定义为线程池的内部类就不需要,实际上 JDK 就定义成了内部类。

至此,任务消费的主流程就已经实现了。

9.2.4.5 实现从队列获取任务

现在来实现 Worker 的 run 方法里面调用的从队列取走任务的方法。

从阻塞队列取走元素本身很简单,调用 take 或者 poll 方法就可以,但在线程池的场景下,需要多考虑两个问题:

  • run 方法里面是一个死循环,只有从队列取不到任务才会跳出循环终止线程,那么哪些场景我们应该 return null,让 run 方法跳出循环?只需要依次考虑线程池的 5 种状态:

    • TERMINATED:这个状态在这里不存在,因为当前线程还活着,而只要有一个线程活着,线程池就不可能 TERMINATED;
    • TIDYING:这个状态在这里也不存在,原因同上,只要有一个线程活着,线程池就不可能 TIDYING;
    • STOP:前面讲过,此状态下队列一定是空的,所以直接 return null;
    • SHUTDOWN:此状态队列里可能还会有任务,所以要判断一下,如果队列没有任务则 return null;
    • RUNNING:正常工作状态,肯定不能直接 return null;
  • 怎么维持核心线程活着,而让超出核心线程数的线程终止?
    • 只需要保存线程总数即可,而且因为会被多线程访问,所以需要保证线程安全;
    • 如果当前线程总数 > 核心线程数,需要考虑 keepAliveTime 参数;
    • 如果当前线程总数 < 核心线程数,一直阻塞等待即可。

根据上面的思路,实现如下:

    private Runnable getTaskFromWorkQueue() {BlockingQueue<Runnable> workQueue = executor.getWorkQueue();//先把要用到的参数读出来int corePoolSize = executor.getCorePoolSize();int maxPoolSize = executor.getMaximumPoolSize();long keepAliveTime = executor.getKeepAliveTime();//需要套一层死循环,不能因为在拿到任务之前被 interrupt 而 return null 出去for (;;) {int ctl = executor.getCtl();int state = LvjcThreadPoolExecutor.runStateOf(ctl);//1.STOP 状态,直接 return nullif (state == LvjcThreadPoolExecutor.STOP) {return null;}//2.SHUTDOWN 状态,需要做个判断,队列为空才能 return null//同时,这里不需要担心,此时判断为空,后面又有任务加入队列,//这种场景不存在,因为是 SHUTDOWN 状态,已经拒绝接受新任务了if (state == LvjcThreadPoolExecutor.SHUTDOWN && workQueue.isEmpty()) {return null;}//3.能走到这里,肯定是 RUNNING 状态int workerCount = LvjcThreadPoolExecutor.workerCountOf(ctl);try {Runnable runnable;//3.1.如果当前线程数量 > 核心线程数,等待 keepAliveTime,还没有任务,就 return null//让当前线程结束,使线程数逐渐缩减到核心线程数,所以调用 poll 方法if (workerCount > corePoolSize) {runnable = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);}//3.2.如果当前工作线程数量 <= 核心线程数,这些线程要保持存活,//所以调用 take 方法,一直阻塞即可else {runnable = workQueue.take();}return runnable;} catch (InterruptedException e) {//任何原因导致的打断异常,全都吃掉,因为上面的代码已经对所有状态做了处理,//所以不会有问题}}}
9.2.4.6 实现提交任务

主要是 execute 接口方法,既然是接口方法,只要按接口规范实现即可:

  • 只有 RUNNING 状态才能提交任务;
  • 优先启用核心线程;
  • 核心线程满了,加入队列;
  • 队列满了,启用备用线程;
  • 备用线程也用完了,执行拒绝策略。
    @Overridepublic void execute(Runnable command) {if (command == null) {throw new NullPointerException();}//有 cas 操作通常就得有自旋for (;;) {//*用一个 AtomicInteger 同时保存线程池状态和线程总数//这个实现参考 JDK,后面讲int c = ctl.get();//1.如果已经 shutdown/stop/terminated,拒绝接受新任务if (runStateOf(c) >= SHUTDOWN) {rejectedExecutionHandler.rejectedExecution(command, this);return;}//2.如果小于 corePoolSize,创建核心线程执行任务int workerCount = workerCountOf(c);if (workerCount < corePoolSize) {try {//创建线程和 shutdown,shutdownNow 方法是互斥的//所以这里加锁处理,不加锁也可以实现,但加锁实现比较容易lock.lock();//线程总数 +1if (compareAndIncrementWorkerCount(c)) {//new 线程同时 startnew Worker(this, threadFactory, command).start();return;}//cas failedcontinue;} finally {lock.unlock();}}//3.如果大于 corePoolSize,队列未满,扔进队列if (workQueue.offer(command)) {return;}//4.队列已满,创建备用线程执行任务//因为距离上次读取线程数已经过去比较久了,//重新读取线程数,降低 cas 失败的概率c = ctl.get();workerCount = workerCountOf(c);if (workerCount < maximumPoolSize) {try {lock.lock();if (compareAndIncrementWorkerCount(c)) {new Worker(this, threadFactory, command).start();return;}} finally {lock.unlock();}}//5.备用线程已用完,拒绝任务rejectedExecutionHandler.rejectedExecution(command, this);return;}}
9.2.4.7 实现 shutdown

shutdown 需要做的是:

  • 修改状态;
  • 打断所有空闲线程;
    @Overridepublic void shutdown() {try {lock.lock();//1.状态改为 shutdownadvanceState(SHUTDOWN);//2.打断所有空闲线程interruptIdleWorkers(false);} finally {lock.unlock();}//3.触发线程池检测是否能 TERMINATEDtryTerminate();}

打断空闲线程的实现:

    /*** 打断空闲线程,会被 shutdown 和 tryTerminate 方法调用,* 其中,shutdown 方法需要打断所有空闲线程;* tryTerminate 方法只需要打断任意一个即可。* @param onlyOne*/private void interruptIdleWorkers(boolean onlyOne) {try {lock.lock();for (Worker worker : workers) {if (worker.isIdle()) {worker.interruptIfStarted();}if (onlyOne) {break;}}} finally {lock.unlock();}}
9.2.4.8 实现 shutdownNow

shutdownNow 需要做的是:

  • 修改状态;
  • 打断所有线程;
  • 把队列清空。
    @Overridepublic List<Runnable> shutdownNow() {List<Runnable> waitingTasks;try {lock.lock();//1.状态改为 stopadvanceState(STOP);//2.打断所有线程,类似于打断空闲线程,只需要把线程是否空闲的判断去掉即可interruptWorkers();//3.清空工作队列,返回工作队列中的任务waitingTasks = drainQueue();} finally {lock.unlock();}//4.也要触发一次 TERMINATED 检测tryTerminate();return waitingTasks;}
9.2.4.9 实现 tryTerminate

因为我们只能 shutdown 一个线程池,而不能 terminate 一个线程池,什么时候应该 terminate 是由线程池自己判断的,所以,我们不能提供 terminate 方法,只能尝试 terminate。

需要考虑,必须满足哪些条件,线程池才可以 terminate:

  • 状态非 RUNNING,确保没有新任务;
  • 队列没有任务;
  • 所有线程空闲。

满足以上三个条件,则意味着所有的任务都处理完了,线程池就可以 terminate 了。

   /*** 检查是否要进入 terminated 状态,* 线程池进入 terminated 状态的情况:* 1.shutdownNow 方法被调用,使线程进入了 stop 状态,并且工作线程数量为 0;* 2.shutdown 方法被调用,使线程进入 shutdown 状态,并且工作线程数量为 0,工作队列里没有任务* ps:这两种情况实际上是同一种情况:工作线程数量为 0,工作队列没有任务,线程池没有在 running*/public void tryTerminate() {//读取线程池状态int c = ctl.get();int state = runStateOf(c);//1.如果线程池还在 running,不满足第一个条件,不能 terminatedif (state == RUNNING) {return;}//2.如果队列还有任务,不满足第二个条件,也不能 terminatedif (!workQueue.isEmpty()) {return;}//3.如果还有线程活着,把空闲线程打断,促使线程快速终止。//这里也可以按照上面分析的第三个条件来实现,即遍历所有线程,//检查是否所有线程都是空闲状态://如果是:就 terminate;//如果不是:就把空闲的线程打断,非空闲的线程等它们跑完。if (workerCountOf(c) != 0) {//这里只需要打断一个空闲线程,//因为能走到这里,说明线程池已经不工作并且工作队列已空,//这种情况打断一个线程后,被打断的线程会跳出循环,结束工作,//然后再次触发 tryTerminate,再打断一个线程,被打断的线程结束,又触发 tryTerminate...interruptIdleWorkers(true);return;}//4.非 running,工作队列没有任务,没有工作线程,进入 terminated 状态try {lock.lock();//更改状态ctl.set(ctlOf(TERMINATED, 0));//5.唤醒在等待 terminate 的线程(有个 awaitTermination 方法)//terminate 成员变量是一个 Condition,//如果没有使用 lock 实现,那么可以参考 FutureTask,//用链表保存所有在等待的线程,然后遍历链表 unpark 唤醒也可以。terminated.signalAll();} finally {lock.unlock();}}
9.2.4.10 实现 awaitTermination

这个实现就很简单,只要在 Condition 类型的 terminate 成员变量上 await 指定的时间即可:

    @Overridepublic boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {//转换为 ns 单位,因为下面的 awaitNanos 方法只接受 ns 时间//也可以不转换,不用 awaitNanos 方法,那就需要自己计算一个 deadlinelong waitNanos = unit.toNanos(timeout);try {lock.lock();for (;;) {//如果在指定时间内,等到了线程池 terminate,返回 trueif (isTerminated()) {return true;}//如果指定时间到了,还没等到线程池 terminate,返回 falseif (waitNanos <= 0) {return false;}//awaitNanos 方法,传入需要等待的总时间,//返回醒来时,剩余还需要等待的时间waitNanos = terminated.awaitNanos(waitNanos);}} finally {lock.unlock();}}
9.2.4.11 实现 isShutdown 和 isTerminated
   /*** 要注意,因为线程池各状态之间存在递进关系,* 只要不是 running 状态,都算 shutdown* @return*/@Overridepublic boolean isShutdown() {return runStateOf(ctl.get()) >= SHUTDOWN;}@Overridepublic boolean isTerminated() {return runStateOf(ctl.get()) >= TERMINATED;}
9.2.4.12 实现 submit 提交任务

除了 execute 方法提交 Runnable 任务,还可以用 submit 提交 Callable 任务。

比较简单,也实现一下:

    @Overridepublic <T> Future<T> submit(Callable<T> task) {RunnableFuture<T> futureTask = new FutureTask<>(task);execute(futureTask);return futureTask;}@Overridepublic <T> Future<T> submit(Runnable task, T result) {RunnableFuture<T> futureTask = new FutureTask<>(task, result);execute(futureTask);return futureTask;}@Overridepublic Future<?> submit(Runnable task) {RunnableFuture<?> futureTask = new FutureTask<>(task, null);execute(futureTask);return futureTask;}

submit 系列方法就是简单地用 FutureTask 封装一下就可以。

invoke 系列方法稍微麻烦一些,但差不多也是一个套路,只是变成批量提交任务,这里就不再自己实现了。

9.2.5 JDK 的实现

实现思路都是一样的,JDK 的实现多考虑了很多小细节,以及某些地方实现更优雅。

9.2.5.1 线程状态与线程数的处理

JDK 使用一个 int 数据同时存储了线程状态和线程数两个信息:

  • 线程状态使用 int 高位 3 个 bit;
  • 线程数使用 int 低位 29 个 bit。
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));private static final int COUNT_BITS = Integer.SIZE - 3;private static final int CAPACITY   = (1 << COUNT_BITS) - 1;// runState is stored in the high-order bitsprivate 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 ctlprivate static int runStateOf(int c)     { return c & ~CAPACITY; }private static int workerCountOf(int c)  { return c & CAPACITY; }private static int ctlOf(int rs, int wc) { return rs | wc; }

这样做会带来一些好处:

  • 省内存;
  • 简洁高效,一个变量,可以用 AtomicInteger 保证原子性,如果用两个变量存储,要么加锁,要么封装成一个对象用 AtomicReference。
9.2.5.2 corePoolSize 可为 0

我们上面自己实现的时候,限制了 corePoolSize 最小是 1,而 JDK 的实现是可以为 0 的。

实际上 Executors 工具类里面的 newCacheThreadPool 就是这么用的:

    public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}

这样就有一个疑问。

前面说,提交新任务时:

  • 优先创建核心线程;
  • 核心线程满,加入队列;
  • 队列满,再继续创建线程;
  • 达到最大线程数,拒绝任务。

那么如果核心线程数为 0,按照上面的说法,它应该会把任务加入队列,等队列放满,而不是创建备用线程来处理。

可以测试真实情况并非如此,看看 JDK 是怎么处理的。

提交任务在 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);//这里,对线程数为 0 的情况做了特殊处理,else if (workerCountOf(recheck) == 0)addWorker(null, false);}else if (!addWorker(command, false))reject(command);}

所以,前面的那个说法并不完全正确。

存在一个特例,队列未满就会创建备用线程,就是核心线程数为 0 的情况。

这跟核心数为 1 还是有所区别的:

  • 第一个任务过来也会先到队列里走一遭;
  • 这个线程实际上还是备用线程,一定时间没有任务,会被销毁。
9.2.5.3 核心线程超时

我们前面没有实现核心线程超时的功能,实际上,ThreadPoolExecutor 是允许核心线程超时的。

    private Runnable getTask() {...    // Are workers subject to culling?boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;...}

在 getTask 方法里面有这么一个处理,如果 allowCoreThreadTimeOut 参数为 true,不管当前线程数是不是大于核心线程数,都会超时销毁。

总结一下核心线程会被销毁的情况:

  • RUNNING 状态,allowCoreThreadTimeOut 参数为 true,超时没有接到任务;
  • SHUTDOWN 状态,队列没有任务;
  • STOP 状态。
9.2.5.4 预创建核心线程

默认情况,核心线程的创建是懒惰模式的,不来任务不创建。

但也支持提前创建。

提前创建一个核心线程:

    public boolean prestartCoreThread() {return workerCountOf(ctl.get()) < corePoolSize &&addWorker(null, true);}

提前创建所有核心线程:

    public int prestartAllCoreThreads() {int n = 0;while (addWorker(null, true))++n;return n;}
9.2.5.5 invokeAll 方法

有两个,一个必须要等所有任务执行完,另一个支持超时提前结束。

这里只看超时方法:

//提交一批任务,等到所有任务执行完成,返回所有任务的 Future,与传入的 Callable 顺序一一对应
//如果超时没有全部完成,提前返回,没有拿到结果的任务全部取消。
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException {if (tasks == null)throw new NullPointerException();long nanos = unit.toNanos(timeout);ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());boolean done = false;try {//1.把所有任务包装成 FutureTaskfor (Callable<T> t : tasks)futures.add(newTaskFor(t));final long deadline = System.nanoTime() + nanos;final int size = futures.size();// Interleave time checks and calls to execute in case// executor doesn't have any/much parallelism.//2.遍历提交所有 FutureTask,在提交 FutureTask 的过程中就可能超时for (int i = 0; i < size; i++) {execute((Runnable)futures.get(i));nanos = deadline - System.nanoTime();if (nanos <= 0L)//超时提前返回结果return futures;}//3.依次阻塞等待获取所有 FutureTask 的执行结果,这里也可能超时for (int i = 0; i < size; i++) {Future<T> f = futures.get(i);if (!f.isDone()) {if (nanos <= 0L)return futures;try {f.get(nanos, TimeUnit.NANOSECONDS);} catch (CancellationException ignore) {//吃掉取消任务异常,避免影响后面的任务} catch (ExecutionException ignore) {//吃掉任务执行失败异常,避免影响后面的任务} catch (TimeoutException toe) {//超时提前返回结果return futures;}nanos = deadline - System.nanoTime();}}//4.走到这里,所有任务都已执行完成done = true;return futures;} finally {if (!done)//5.如果任务没有来得及全部执行完成,cancel(true) 所有任务//回顾一下上一章讲过的 cancel(true)://对已经有结果的任务(NORMAL/EXCEPTIONAL 状态),没有任何影响;//对尚未执行的任务(NEW 状态),置为 INTERRUTED 最终态,任务不会再执行;//对正在执行的任务(NEW 状态),将其打断,置为 INTERRUPTED 最终态,//    如果任务响应打断,抛出 InterruptedException,但不会保存结果;//    如果任务不响应打断,继续执行,但执行完也不会保存结果。for (int i = 0, size = futures.size(); i < size; i++)futures.get(i).cancel(true);}}
9.2.5.6 invokeAny 方法

invokeAny 方法也有两个,其中一个支持超时:

    //批量提交一批任务,有任意一个成功执行完成,都会返回,返回值为任务结果//超时未得到结果抛异常,取消所有任务public <T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException {return doInvokeAny(tasks, true, unit.toNanos(timeout));}

主要逻辑在 doInvokeAny 方法:

private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,boolean timed, long nanos)throws InterruptedException, ExecutionException, TimeoutException {if (tasks == null)throw new NullPointerException();int ntasks = tasks.size();if (ntasks == 0)throw new IllegalArgumentException();ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);//注意这里用了上一章讲过的 CompletionServiceExecutorCompletionService<T> ecs =new ExecutorCompletionService<T>(this);// For efficiency, especially in executors with limited// parallelism, check to see if previously submitted tasks are// done before submitting more of them. This interleaving// plus the exception mechanics account for messiness of main// loop.try {// Record exceptions so that if we fail to obtain any// result, we can throw the last exception we got.ExecutionException ee = null;final long deadline = timed ? System.nanoTime() + nanos : 0L;Iterator<? extends Callable<T>> it = tasks.iterator();// Start one task for sure; the rest incrementally//1.先提交第一个任务futures.add(ecs.submit(it.next()));--ntasks;int active = 1;//2.循环提交剩下的任务for (;;) {//2.1.在提交新任务之前,先尝试读取一下已经提交的任务有没有已经完成的//ecs.poll() 会从任务执行结果队列里面尝试取出第一个结果Future<T> f = ecs.poll();//2.2.如果所有已经提交的任务都还未完成if (f == null) {//2.2.1.如果还有任务没有提交if (ntasks > 0) {//再提交一个任务看看--ntasks;futures.add(ecs.submit(it.next()));++active;}//2.2.2.如果所有任务都已经提交,并且都已经执行完成,但又没拿到结果else if (active == 0)//跳出循环,没机会了,所有任务全都抛异常,没有一个成功执行完的break;//2.2.3.如果所有任务都已经提交,但还未全部执行完,而且有超时时间else if (timed) {//在超时之前,等一个结果f = ecs.poll(nanos, TimeUnit.NANOSECONDS);//时间到,醒来没结果,超时异常//不要担心被 interrupt,interrupt 会抛异常,不会走到这里if (f == null)throw new TimeoutException();//时间还没到,提前拿到结果,继续往下跑nanos = deadline - System.nanoTime();}//2.2.4.如果所有任务都已经提交,但还未全部完成,而且没有超时时间else//一直等,等到出结果为止f = ecs.take();}//2.3.拿到了一个任务的结果//可能是在 2.1 那一步拿到的;//也可能是在 2.2.3,2.2.4 那两处拿到的if (f != null) {--active;try {//把结果取出来,这里的 get 不会阻塞,//CompletionService 里面取出来的一定是执行完成的//如果结果是成功执行的,就在这里返回了return f.get();} catch (ExecutionException eex) {//这是个抛异常的结果,保存异常,继续循环等下一个任务结果ee = eex;} catch (RuntimeException rex) {//其它为止运行时异常,保存异常,继续循环等下一个任务结果ee = new ExecutionException(rex);}}}//3.所有任务的结果都已经出来了,但没有在上面 return 出去//说明所有任务全都失败了if (ee == null)ee = new ExecutionException();throw ee;} finally {//4.拿到正确的结果,或者超时,或者所有任务全都失败//不管怎样,cancel(true) 所有任务for (int i = 0, size = futures.size(); i < size; i++)futures.get(i).cancel(true);}}
9.2.5.7 钩子方法

JDK 的实现里面还留下了一些 hook 方法便于子类扩展。

主要有三个:

  • beforeExecute 方法:在任务执行之前调用,默认空实现;
  • afterExecute 方法:在任务之后调用,默认空实现;
  • terminated 方法:在线程池进入 TERMINATED 状态之前调用,默认空实现。

注意:以上三个方法里面如果抛出异常,外面不会 catch,会在 finally 逻辑执行完后再抛出去。

9.2.5.8 拒绝策略

ThreadPoolExecutor 已经提供了 4 个 RejectedExecutionHandler。

    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());}}
     public static class CallerRunsPolicy implements RejectedExecutionHandler {public CallerRunsPolicy() { }public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {//线程池如果 shutdown 不处理//也就是只处理因队列满和线程满而来不及处理的任务;//因线程池 shutdown 而被拒绝的任务直接丢弃if (!e.isShutdown()) {//在调用方线程直接调 run 方法r.run();}}}
    public static class DiscardPolicy implements RejectedExecutionHandler {public DiscardPolicy() { }//丢弃任务,啥都不干public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}}
public static class DiscardOldestPolicy implements RejectedExecutionHandler {public DiscardOldestPolicy() { }public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {//也是不处理因 shutdown 而被拒绝的任务if (!e.isShutdown()) {//把对列头部的任务挤掉一个e.getQueue().poll();e.execute(r);}}
}

Java并发系列(11)——ThreadPoolExecutor实现原理与手写相关推荐

  1. 5W字高质量java并发系列详解教程(上)-附PDF下载

    文章目录 第一章 java.util.concurrent简介 主要的组件 Executor ExecutorService ScheduledExecutorService Future Count ...

  2. Java并发系列(10)——FutureTask 和 CompletionService

    接上一篇<Java并发系列(9)--并发工具类> 文章目录 8 FutureTask 与 CompletionService 8.1 FutureTask 8.1.1 类图 8.1.2 几 ...

  3. Java并发机制的底层实现原理

    Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令.本章我们将 ...

  4. 《Java并发编程的艺术》一一第2章Java并发机制的底层实现原理

    第2章Java并发机制的底层实现原理 2.1 volatile的应用 Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行, ...

  5. 《Java并发编程的艺术》:第2章 Java并发机制的底层实现原理

    前言 Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节 码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和 CPU的指令. ...

  6. 【deep_thoughts】30_PyTorch LSTM和LSTMP的原理及其手写复现

    文章目录 LSTM API 手写 lstm_forward 函数 LSTMP 修改 lstm_forward 函数 视频链接: 30.PyTorch LSTM和LSTMP的原理及其手写复现_哔哩哔哩_ ...

  7. 理解 call、apply、bind 原理,手写简单的 call、apply、bind 方法

    理解 call.apply.bind 原理,手写简单的 call.apply.bind 方法 call 原理及实现 MDN定义:call()方法使用给定的 this 值和单独提供的参数调用函数. 用自 ...

  8. java多线程系列:ThreadPoolExecutor源码分析

    前言 这篇主要讲述ThreadPoolExecutor的源码分析,贯穿类的创建.任务的添加到线程池的关闭整个流程,让你知其然所以然.希望你可以通过本篇博文知道ThreadPoolExecutor是怎么 ...

  9. java多线程系列:ThreadPoolExecutor源码分析,java基础面试笔试题

    我总结出了很多互联网公司的面试题及答案,并整理成了文档,以及各种学习的进阶学习资料,免费分享给大家. 扫描二维码或搜索下图红色VX号,加VX好友,拉你进[程序员面试学习交流群]免费领取.也欢迎各位一起 ...

最新文章

  1. Logstash完成ELK集群
  2. 【转】C#中的命名空间namespace全解
  3. js删除数组中的某个对象
  4. 箭头函数的this指向谁_你好,我是 JavaScript 的 this
  5. android 网络错误分析,android wifi打开过程源码解析及Wifi打开失败原因分析
  6. 一句一句的读ArrayList源码(代码基于JDK11)
  7. 七、文章管理页面及功能实现《iVX低代码/无代码个人博客制作》
  8. ENVI5.1 进行监督分类流程化工具时(classification workflow)界面显示不全的问题解决办法
  9. Hook技术(1):Hook技术简介
  10. 软件测试人员如何月薪过万、月薪过万的秘籍
  11. 商业智能在医疗卫生领域的应用
  12. JS实现图片不存在时显示默认图片
  13. BlackBerry 模拟器上网
  14. 雷霆战机单机老版本_雷霆战机下载_雷霆战机电脑版单机游戏下载
  15. 基于区块链技术实现“资产通证化”
  16. 企业实施WMS仓储管理系统需要规避哪些风险
  17. IT加油站课程调价通告
  18. SEIR传染病模型Netlogo仿真程序
  19. SAP 接口开发技术和工具
  20. java getclass()函数

热门文章

  1. leetcode 手写计算器 方法总结
  2. C-TPAT认证辅导,加入CTPAT将与CBP达成协议以保护供应链
  3. MQTT协议应用场景1: 外网手机客户端 与 内网树莓派3B 的通讯
  4. 网页拼图游戏(html css js)实现
  5. 基于多目标遗传算法的IEEE14节点系统分布式电源选址定容matlab程序
  6. 数据压缩作业2——TGA格式文件分析
  7. 栈和队列6:滑动窗口最大值
  8. 进程是否采用3d指令的判断
  9. selenium 执行完后自动退出浏览器窗口问题解决
  10. 【原创】思维导图写测试用例的再补充