java高级面试题及答案 博客园,一文读懂线程池的实现原理,2024火爆全网系列

ctl 这个 AtomicInteger 类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高 3 位保存 runState,低 29 位保存 workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多。

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

在这里插入图片描述

哇,Doug Lea 大佬简直了,设计的真好。老周等等我,这里怎么设计的就好了?CAPACITY 这里是多少呀?

不着急,老周这就带你来分析分析为什么一个整型变量既可以保存运行状态,又可以保存线程数量?

首先,我们知道 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 位表示线程池中有效的线程数。

在这里插入图片描述

这里你有可能问了,老周啊,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 所定义的状态,这些状态都和线程的执行密切相关:

在这里插入图片描述

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

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

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

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

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

在这里插入图片描述

3.3 线程池如何管理任务

3.3.1 任务调度

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

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

threadPool.execute(new Job());

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

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

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

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

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

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

执行流程图如下:

在这里插入图片描述

3.3.2 待执行任务的队列

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

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

这两个附加的操作是:

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

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

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

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

在这里插入图片描述

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

在这里插入图片描述

3.3.3 任务申请

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

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

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

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

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

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

在这里插入图片描述

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

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

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

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

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

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

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

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

3.3.4 任务拒绝

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

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

在这里插入图片描述

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

在这里插入图片描述

3.4 线程池如何管理线程

3.4.1 Worker线程

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

在这里插入图片描述

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

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

3.4.1.1 AQS 作用

Worker 继承了 AbstractQueuedSynchronizer,主要目的有两个:

  • 将锁的粒度细化到每个 Worker

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

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

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

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

这份清华大牛整理的进大厂必备的redis视频、面试题和技术文档

祝大家早日进入大厂,拿到满意的薪资和职级~~~加油!!

感谢大家的支持!!

image.png

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!**](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值