线程池核心原理+动态调整线程池参数的实践,一次帮你搞定面试必问的线程池

总结

无论是哪家公司,都很重视高并发高可用的技术,重视基础,重视JVM。面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。其实我写了这么多,只是我自己的总结,并不一定适用于所有人,相信经过一些面试,大家都会有这些感触。

最后我整理了一些面试真题资料,技术知识点剖析教程,还有和广大同仁一起交流学习共同进步,还有一些职业经验的分享。

面试了阿里,滴滴,网易,蚂蚁,最终有幸去了网易【面试题分享】

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

ExecutorService 接口增加了一些能力:

扩充执行任务的能力,补充可以为一个或一批异步任务生成 Future 的方法;

提供了管控线程池的方法,比如停止线程池的运行。

AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类 ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

我们来看下 ThreadPoolExecutor 的运行流程:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。

任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:

直接申请线程执行该任务

缓冲到队列中等待线程执行

拒绝该任务

线程管理部分充当消费者的角色,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。

下面就从以下三个核心机制来详细讲解线程池运行机制:

  • 线程池如何维护自身状态

  • 线程池如何管理任务

  • 线程池如何管理线程

2.2 线程池如何维护自身状态

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量(workerCount)。

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

ctl 这个 AtomicInteger 类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高 3 位保存 runState,低 29 位保存 workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。

通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

关于内部封装的获取生命周期状态、获取线程池线程数量的计算方法如下代码:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

为什么一个整型变量既可以保存运行状态,又可以保存线程数量?

首先,我们知道 Java 中 1 个整型占 4 个字节,也就是 32 位,所以 1 个整型有 32 位。

所以整型 1 用二进制表示就是:0000 0000 0000 0000 0000 0000 0000 0001

整型 -1 用二进制表示就是:1111 1111 1111 1111 1111 1111 1111 1111 (这个是补码,这个忘了的话那得去复习下原码、反码、补码等计算机基础知识了。)

在 ThreadPoolExecutor,整型中 32 位的前 3 位用来表示线程池状态,后 29 位表示线程池中有效的线程数。

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

CAPACITY = (1 << 29) - 1 得到 0001 1111 1111 1111 1111 1111 1111 1111,带你分析下 CAPACITY 怎么来的,下面的那些状态大家也可以自己去分析一下。

我们先来看 1 << 29,首先看 1 的二进制代表 0000 0000 0000 0000 0000 0000 0000 0001。

然后 0000 0000 0000 0000 0000 0000 0000 0001 向左移 29 位,得到 0010 0000 0000 0000 0000 0000 0000 0000。

最后将 0010 0000 0000 0000 0000 0000 0000 0000 减 1 得到 0001 1111 1111 1111 1111 1111 1111 1111。

我们下面再来了解下 ThreadPoolExecutor 所定义的状态,这些状态都和线程的执行密切相关:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务。

SHUTDOWN:指调用了 shutdown() 方法,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。

STOP:指调用了 shutdownNow() 方法,不再接受新提交的任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。

TIDYING: 所有任务都执行完毕,workerCount 有效线程数为 0。

TERMINATED:终止状态,当执行 terminated() 后会更新为这个状态。

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

2.3 线程池如何管理任务

2.3.1 任务调度

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。

首先,所有任务的调度都是由 execute 方法完成的,比如我们业务代码中

threadPool.execute(new Job());。

这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务。

如果 workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

如果 workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

如果 workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

如果 workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常。

执行流程图如下:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

2.3.2 待执行任务的队列

待执行任务的队列是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。

这两个附加的操作是:

在队列为空时,获取元素的线程会等待队列变为非空。

当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

下图中展示了 Thread1 往阻塞队列中添加元素,而线程 Thread2 从阻塞队列中移除元素:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

使用不同的队列可以实现不一样的任务存取策略。我们下面来看下阻塞队列的成员:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

2.3.3 任务申请

从上文可知,任务的执行有两种可能:

一种是任务直接由新创建的线程执行

另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。

第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

线程需要从待执行任务的队列中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。

这部分策略由 getTask 方法实现,我们来看下 getTask 方法的代码。

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

getTask 方法在阻塞队列中有待执行的任务时会从队列中弹出一个任务并返回,如果阻塞队列为空,那么就会阻塞等待新的任务提交到队列中直到超时(在一些配置下会一直等待而不超时),如果在超时之前获取到了新的任务,那么就会将这个任务作为返回值返回。所以一般 getTask 方法是不会返回 null 的,只会阻塞等待下一个任务并在之后将这个新任务作为返回值返回。

当 getTask 方法返回 null 时会导致当前 Worker 退出,当前线程被销毁。在以下情况下 getTask 方法才会返回 null:

当前线程池中的线程数超过了最大线程数。这是因为运行时通过调用 setMaximumPoolSize 修改了最大线程数而导致的结果;

线程池处于 STOP 状态。这种情况下所有线程都应该被立即回收销毁;

线程池处于 SHUTDOWN 状态,且阻塞队列为空。这种情况下已经不会有新的任务被提交到阻塞队列中了,所以线程应该被销毁;

线程可以被超时回收的情况下等待新任务超时。线程被超时回收一般有以下两种情况:

允许核心线程超时(线程池配置)的情况下线程等待任务超时

超出核心线程数部分的线程等待任务超时

2.3.4 任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到 maximumPoolSize 时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

拒绝策略是一个接口,其设计如下:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

用户可以通过实现这个接口去定制拒绝策略,也可以选择 JDK 提供的四种已有拒绝策略,其特点如下:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

2.4 线程池如何管理线程

2.4.1 Worker线程

线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程 Worker。我们来看一下它的代码:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

Worker 这个工作线程,实现了 Runnable 接口,并持有一个线程thread,一个初始化的任务firstTask。thread 是在调用构造方法时通过 ThreadFactory 来创建的线程,可以用来执行任务;

firstTask 用它来保存传入的第一个任务,这个任务可以有也可以为 null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是空的,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

2.4.1.1 AQS 作用

Worker 继承了

AbstractQueuedSynchronizer,主要目的有两个:

将锁的粒度细化到每个 Worker

如果多个 Worker 使用同一个锁,那么一个 Worker Running 持有锁的时候,其他 Worker 就无法执行,这显然是不合理的。

直接使用 CAS 获取,避免阻塞。

如果这个锁使用阻塞获取,那么在多 Worker 的情况下执行 shutDown。如果这个 Worker 此时正在 Running 无法获取到锁,那么执行 shutDown() 线程就会阻塞住了,显然是不合理的。

2.4.1.2 Runnable 作用

Worker 还实现了 Runnable,它有两个属性 thead、firstTask。

firstTask 用它来保存传入的第一个任务,这个任务可以有也可以为 null。

如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况。

如果这个值是 null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

根据整体流程:

线程池调用 execute —> 创建 Worker(设置属性thead、firstTask)—> worker.thread.start() —> 实际上调用的是 worker.run() —> 线程池的 runWorker(worker) —> worker.firstTask.run() (如果 firstTask 为 null 就从等待队列中拉取一个)。

Worker 执行任务的模型如下图所示:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

2.4.2 Worker 线程增加

增加线程是通过线程池中的 addWorker 方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。

addWorker 方法有两个参数:firstTask、core。

firstTask 参数用于指定新增的线程执行的第一个任务,该参数可以为空;

core 参数为 true 表示在新增线程时会判断当前活动线程数是否少于 corePoolSize,false 表示新增线程前需要判断当前活动线程数是否少于 maximumPoolSize。

我们来看一下 addWorker 的源码:

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

源码看着是不是挺费劲的?没关系,再看一张执行流程图加深下印象。

你敢信?掌握Java线程池原理,面试官会主动为你加薪,受宠若惊

2.4.3 Worker 线程执行任务

Worker 中的线程 start 的时候,调用 Worker 本身 run 方法,这个 run 方法调用外部类ThreadPoolExecutor 的 runWorker 方法,直接看 runWorker 方法的源码:

那么如何才能正确的掌握Redis呢?

为了让大家能够在Redis上能够加深,所以这次给大家准备了一些Redis的学习资料,还有一些大厂的面试题,包括以下这些面试题

  • 并发编程面试题汇总

  • JVM面试题汇总

  • Netty常被问到的那些面试题汇总

  • Tomcat面试题整理汇总

  • Mysql面试题汇总

  • Spring源码深度解析

  • Mybatis常见面试题汇总

  • Nginx那些面试题汇总

  • Zookeeper面试题汇总

  • RabbitMQ常见面试题汇总

JVM常频面试:

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Mysql面试题汇总(一)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Mysql面试题汇总(二)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

Redis常见面试题汇总(300+题)

Redis高频面试笔记:基础+缓存雪崩+哨兵+集群+Reids场景设计

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • Nginx那些面试题汇总

  • Zookeeper面试题汇总

  • RabbitMQ常见面试题汇总

JVM常频面试:

[外链图片转存中…(img-XkzhQ4hu-1715700651351)]

Mysql面试题汇总(一)

[外链图片转存中…(img-jsdosb6f-1715700651351)]

Mysql面试题汇总(二)

[外链图片转存中…(img-OPxZE5B5-1715700651351)]

Redis常见面试题汇总(300+题)

[外链图片转存中…(img-s1Rw3J7X-1715700651352)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值