Java 的线程池工作过程

本文由 简悦 SimpRead 转码, 原文地址 www.bilibili.com

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在 Java 中可以通过线程池来达到这样的效果。今天我们就来详细讲解一下 Java 的线程池,首先我们从最核心的 ThreadPoolExecutor 类中的方法讲起,然后再讲述它的实现原理,接着给出了它的使用示例,最后讨论了一下如何合理配置线程池的大小。

java.uitl.concurrent.ThreadPoolExecutor  类是线程池中最核心的一个类,通过查看源码, 我们可以知道这个类继承了 AbstractExecutorService 抽象类.

下面我们来看一下 ThreadPoolExecutor 类中的构造方法:

  • ThreadPoolExecutor 类中共有 4 个构造方法, 前三个构造方法其实最终调用的都是最后一个构造方法, 也就是下面这个参数最全的构造方法.

  • 构造方法

public ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue<Runnable> workQueue,

ThreadFactory threadFactory,

RejectedExecutionHandler handler){......}

接下来我们来看一下这个构造器中, 每个参数的含义分别是什么:

  • corePoolSize

  • 核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了 prestartAllCoreThreads() 或者 prestartCoreThread() 方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建 corePoolSize 个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中;

  • maximumPoolSize

  • 线程池中最大线程数, 用来表示线程池中最多能创建多少个线程.

  • keepAliveTime

  • 线程的存活时间, 表示线程没有任务执行时最多保持多久时间会终止。

  • 默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,直到线程池中的线程数不超过 corePoolSize。但是如果调用了 allowCoreThreadTimeOut(boolean) 方法,在线程池中的线程数不大于 corePoolSize 时,keepAliveTime 参数也会起作用,直到线程池中的线程数为 0;

  • unit

  • TimeUnit.DAYS;               // 天

  • TimeUnit.HOURS;             // 小时

  • TimeUnit.MINUTES;           // 分钟

  • TimeUnit.SECONDS;           // 秒

  • TimeUnit.MILLISECONDS;      // 毫秒

  • TimeUnit.MICROSECONDS;      // 微妙

  • TimeUnit.NANOSECONDS;       // 纳秒

  • 参数 keepAliveTime 的时间单位,有 7 种取值,在 TimeUnit 类中有 7 种静态属性:

  • workQueue

  • ArrayBlockingQueue;

  • 一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

  • LinkedBlockingQueue;

  • SynchronousQueue;

  • threadFactory

  • 线程工厂,用来创建线程,主要是为了给线程起名字,默认工厂的线程名字 pool-1-thread-3。

  • Handler

  • ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出 RejectedExecutionException 异常。

  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

  • 拒绝策略,当线程池里线程被耗尽,且队列也满了的时候会调用。

以上就是创建线程池时用到的几个参数,面试中经常会有面试官问到这些参数的含义.

下图为线程池的执行流程

  1. 如果当前工作线程数小于核心线程,则创建核心线程执行任务。

  2. 如果当前线程大于核心线程数则判断等待队列是否已满,如果没有满则添加任务到等待队列中去,如果工作线程数量为 0 则创建非核心线程,并从等待队列中拉取任务执行。

  3. 最后如果队列已满创建一个非核心线程执行任务。如果创建失败则会拒绝任务。

ThreadPoolExecutor 是线程池的实现类, 无论是自定义线程池, 还是使用系统提供的线程池, 都会使用到这个类. 通过类的 execute(Runnable command) 方法来执行 Runnable 任务.

  1. 判断当前活跃线程数是否小于 corePoolSize, 如果小于,则调用 addWorker 创建线程执行任务

  2. 如果不小于 corePoolSize,则将任务添加到 workQueue 队列。

  3. 如果放入 workQueue 失败,则创建线程执行任务,如果这时创建线程失败 (当前线程数不小于 maximumPoolSize 时),就会调用 reject(内部调用 handler) 拒绝接受任务。

在 execute() 方法中获知通过 addWorker() 方法来添加新线程, 那么到底是如何添加和管理的?

这块代码是在创建非核心线程时,即 core 等于 false。判断当前线程数是否大于等于 maximumPoolSize,如果大于等于则返回 false,即上边说到的 3 中创建线程失败的情况。

创建 worker 对象, 并将 Runnable 作为参数传递进去, 并从 worker 中取出 Thread 对象, 进行一系列条件判断后.

开启 Thread 的 start() 方法, 线程开始运行. 所以 worker 对象中必然包含了一个 Thread 和一个要被执行的 Runnable.

  • 每个 worker, 都是一条线程, 同时里面包含了一个 firstTask, 即初始化时要被首先执行的任

  • 最终执行任务的, 是 runWorker() 方法

线程调用 runWoker,会 while 循环调用 getTask 方法从 workerQueue 里读取任务,然后执行任务。只要 getTask 方法不返回 null, 此线程就不会退出。

  • 先不管 allowCoreThreadTimeOut,这个变量默认值是 false。wc>corePoolSize 则是判断当前线程数是否大于 corePoolSize。

  • 如果当前线程数大于 corePoolSize,则会调用 workQueue 的 poll 方法获取任务,超时时间是 keepAliveTime。如果超过 keepAliveTime 时长,poll 返回了 null,上边提到的 while 循序就会退出,线程也就执行完了。

最近收集整理一份面试资料,覆盖了 Java 核心技术、JVM、Java 并发、SSM、微服务、数据库、数据结构等等技术点, 有兴趣的同学可以 + VX: congqing24 获取相关资料.

posted @ 2022-09-20 16:41  托马斯布莱克  阅读(40)  评论(0编辑  收藏  举报