看到一个魔改线程池,面试素材加一!(中)

简介: 看到一个魔改线程池,面试素材加一!(中)

KeyAffinityExecutor用法


先说说这个类的用法吧。

其对应的开源项目地址是这个:

https://github.com/PhantomThief/more-lambdas-java

如果你想把它用起来,得引入下面这个 maven 地址:

<dependency>
    <groupId>com.github.phantomthief</groupId>
    <artifactId>more-lambdas</artifactId>
    <version>0.1.55</version>
</dependency>

其核心代码是这个接口:

com.github.phantomthief.pool.KeyAffinityExecutor

这个接口里面有大量的注释,大家可以拉下来看一下。

我这里主要给大家看一下接口上面,作者写的注释,他是这样介绍自己的这个工具的。

这是一个按指定的 Key 亲和顺序消费的线程池。

KeyAffinityExecutor 是一个特殊的任务线程池。

它可以确保投递进来的任务按 Key 相同的任务依照提交顺序依次执行。在既要通过并行处理来提高吞吐量、又要保证一定范围内的任务按照严格的先后顺序来运行的场景下非常适用。

KeyAffinityExecutor 的内建实现方式,是将指定的 Key 映射到固定的单线程线程池上,它内部会维护多个(数量可配)这样的单线程线程池,来保持一定的任务并行度。

需要注意的是,此接口定义的 KeyAffinityExecutor,并不要求 Key 相同的任务在相同的线程上运行,尽管实现类可以按照这种方式来实现,但它并非一个强制性的要求,因此在使用时也请不要依赖这样的假定。

很多人问,这和自己使用一个线程池的数组,并通过简单取模的方式来实现有什么区别?

事实上,大多数场景的确差异不大,但是当数据倾斜发生时,被散列到相同位置的数据可能会因为热点倾斜数据被延误。

本实现在并发度较低时(阈值可设置),会挑选最闲置的线程池投递,尽最大可能隔离倾斜数据,减少对其它数据带来的影响。

在作者的这段介绍里面,简单的说明了该项目的应用场景和内部原理,和我们前面分析的差不多。

除此之外,还有两个需要特别注意的地方。

第一个地方是这里:

image.png

作为区分的任务维度的对象,如果是自定义对象,那么一定要重写其 hashCode、equals,以确保可以起到标识作用。

这一处的提醒就和 HashMap 的 key 如果是对象的话,应该要重写 hashCode、equals 方法的原因是一样一样的。

编程基础,只提一下,不多赘述。

第二个地方得好好说一下,属于他的核心思想。

他没有采用简单取模的方式,因为在简单取模的场景上,数据是有可能发生倾斜的。

我个人是这样理解作者的思路的。

首先说明一下取模的数据倾斜是咋回事,举个简单的例子:

image.png


上面的代码片段中,我加入了一个新角色“摸鱼大师”。同时给对象新增了一个 id 字段。

假设,我们对 id 字段用 2 取余:

image.png

那么会出现的情况就是大师和富贵对应的 id 取余结果都是 1,它们将同用一个线程池。

很明显,由于大师的频繁操作,导致“摸鱼”变成了热点数据,从而导致编号为 0 的连接池发了倾斜,进而影响到了富贵的正常工作。

而 KeyAffinityExecutor 的策略是什么样的呢?

它会挑选最闲置的线程池进行投递。

怎么理解呢?

还是上面的例子,如果我们构建这样的线程池:

KeyAffinityExecutor executorService =
                KeyAffinityExecutor.newSerializingExecutor(3, 200, "MY-POOL-%d");

第一个参数 3,代表它会在这里线程池里面构建 3 个只有一个线程的线程池。

那么当用它来提交任务的时候,由于维度是 id 维度,我们刚好三个 id,所以刚好把这个线程池占满:

image.png

这个时候是不存在数据倾斜的。

但是,如果我把前面构建线程池的参数从 3 变成 2 呢?

KeyAffinityExecutor executorService =
                KeyAffinityExecutor.newSerializingExecutor(2, 200, "MY-POOL-%d");

提交方式不变,里面加上对 id 为 1 和 2 的任务延迟的逻辑,目的是观察 id 为 3 的数据怎么处理:

image.png

毋庸置疑,当提交执行大师的摸鱼操作的时候线程池肯定不够用了,怎么办?

这个时候,根据作者描述“会挑选最闲置的线程池投递”。

我用这样的数据来说明:

image.png

所以,当执行大师摸鱼操作的时候,会去从仅有的两个选项中选一个出来。

怎么选?

谁的并发度低,就选谁。

由于有延迟时间在任务里面,所以我们可以观察到执行富贵的线程的并发度是 5,而执行旺财的线程的并发度是 6。

因此执行大师的摸鱼操作的时候,会选择并发度为 5 的线程进行处理。

image.png

这个场景下就出现了数据倾斜。但是倾斜的前提发生了变化,变成了当前已经没有可用线程了。

所以,作者说“尽最大可能隔离倾斜数据”。

这两个方案最大的差异就是对线程资源的利用程度,如果是单纯的取模,那么有可能出现发生数据倾斜的时候,还有可用线程。

如果是 KeyAffinityExecutor 的方式,它可以保证发生数据倾斜的时候,线程池里面的线程一定是已经用完了。

然后,你再品一品这两个方案之间的细微差异。


KeyAffinityExecutor源码


源码不算多,一共就这几个类:

image.png

但是他的源码里面绝大部分都是 lambdas 的写法,基本上都是函数式编程,如果你对这方面比较薄弱的话那么看起来会比较吃力一点。

如果你想掌握其源码的话,我建议是把项目拉到本地,然后从他的测试用例入手:

https://github.com/PhantomThief/more-lambdas-java


image.png

我给大家汇报一下我看到的一些关键的地方,方便大家自己去看的时候梳理思路。

首先肯定是从它的构造方法入手,每一个入参的含义作者都标注的非常清楚了:

image.png

假设我们的构造函数是这样的,含义是构建 3 个只有一个线程的线程池,每个线程池的队列大小是 200:

KeyAffinityExecutor executorService =
                KeyAffinityExecutor.newSerializingExecutor(3, 200, "WHY-POOL-%d");

首先我们要找到构建“只有一个线程的线程池”的逻辑在哪。

就藏在构造函数里面的这个方法:

com.github.phantomthief.pool.KeyAffinityExecutorUtils#executor(java.lang.String, int)

在这里可以看到我们一直提到的“只有一个线程的线程池”,队列的长度也可以指定:

image.png

该方法返回的是一个 Supplier 接口,等下就要用到。

接下来,我们要找到 “3” 这个数字是体现在哪儿的呢?

就藏在构造函数的 build 方法里面,该方法最终会调用到这个方法来:

com.github.phantomthief.pool.impl.KeyAffinityImpl#KeyAffinityImpl

目录
相关文章
|
6天前
|
监控 Kubernetes Java
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
|
2天前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
43 16
|
11天前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
51 11
|
10天前
|
Java Linux 调度
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
28 6
|
15天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
29天前
|
缓存 安全 Java
【JavaEE】——单例模式引起的多线程安全问题:“饿汉/懒汉”模式,及解决思路和方法(面试高频)
单例模式下,“饿汉模式”,“懒汉模式”,单例模式下引起的线程安全问题,解锁思路和解决方法
|
29天前
|
Java 调度
|
4月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
|
4月前
|
消息中间件 前端开发 NoSQL
面试官:线程池遇到未处理的异常会崩溃吗?
面试官:线程池遇到未处理的异常会崩溃吗?
94 3
面试官:线程池遇到未处理的异常会崩溃吗?
|
2月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!