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

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

今天给大家分享一个经过扩展后的线程池,且我觉得扩展的思路非常好的。

放心,我标题党来着,我觉得面试不会有人考这个玩意,但是工作中是有可能真的会遇到相应的场景。

为了引出这个线程池,我先给大家搞个场景,方便理解。

就拿下面这个表情包来做例子吧。

微信图片_20220428222809.jpg


假设我们有两个程序员,就叫富贵和旺财吧。

上面这个表情包就是这两个程序员一天的工作写照,用程序来表示是这样的。

首先我们搞一个对象,表示程序员当时正在做的事儿:

public class CoderDoSomeThing {
    private String name;
    private String doSomeThing;
    public CoderDoSomeThing(String name, String doSomeThing) {
        this.name = name;
        this.doSomeThing = doSomeThing;
    }
}

然后,用代码描述一下富贵和旺财做的事儿:

public class NbThreadPoolTest {
    public static void main(String[] args) {
        CoderDoSomeThing rich1 = new CoderDoSomeThing("富贵", "启动Idea");
        CoderDoSomeThing rich2 = new CoderDoSomeThing("富贵", "搞数据库,连tomcat,crud一顿输出");
        CoderDoSomeThing rich3 = new CoderDoSomeThing("富贵", "嘴角疯狂上扬");
        CoderDoSomeThing rich4 = new CoderDoSomeThing("富贵", "接口访问报错");
        CoderDoSomeThing rich5 = new CoderDoSomeThing("富贵", "心态崩了,卸载Idea");
        CoderDoSomeThing www1 = new CoderDoSomeThing("旺财", "启动Idea");
        CoderDoSomeThing www2 = new CoderDoSomeThing("旺财", "搞数据库,连tomcat,crud一顿输出");
        CoderDoSomeThing www3 = new CoderDoSomeThing("旺财", "嘴角疯狂上扬");
        CoderDoSomeThing www4 = new CoderDoSomeThing("旺财", "接口访问报错");
        CoderDoSomeThing www5 = new CoderDoSomeThing("旺财", "心态崩了,卸载Idea");
    }
}

简单解释一下变量的名称,表明我还是经过深思熟虑了的。

富贵,就是有钱,所以变量名叫做 rich。

旺财,就是汪汪汪,所以变量名叫做 www。

你看我这个类的名称,NbThreadPoolTest,就知道我是要用到线程池了。

实际情况中,富贵和旺财两个人是可以各干各的事儿,互不干扰的,也就是他们应该是各自的线程。

各干各的事儿,互不干扰,这听起来好像是可以用线程池的。

所以,我把程序修改成了下面这个样子,把线程池用起来:

public class NbThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        List<CoderDoSomeThing> coderDoSomeThingList = new ArrayList<>();
        coderDoSomeThingList.add(new CoderDoSomeThing("富贵", "启动Idea"));
        coderDoSomeThingList.add(new CoderDoSomeThing("富贵", "搞数据库,连tomcat,crud一顿输出"));
        coderDoSomeThingList.add(new CoderDoSomeThing("富贵", "嘴角疯狂上扬"));
        coderDoSomeThingList.add(new CoderDoSomeThing("富贵", "接口访问报错"));
        coderDoSomeThingList.add(new CoderDoSomeThing("富贵", "心态崩了,卸载Idea"));
        coderDoSomeThingList.add(new CoderDoSomeThing("旺财", "启动Idea"));
        coderDoSomeThingList.add(new CoderDoSomeThing("旺财", "搞数据库,连tomcat,crud一顿输出"));
        coderDoSomeThingList.add(new CoderDoSomeThing("旺财", "嘴角疯狂上扬"));
        coderDoSomeThingList.add(new CoderDoSomeThing("旺财", "接口访问报错"));
        coderDoSomeThingList.add(new CoderDoSomeThing("旺财", "心态崩了,卸载Idea"));
        coderDoSomeThingList.forEach(coderDoSomeThing -> {
            executorService.execute(() -> {
                System.out.println(coderDoSomeThing.toString());
            });
        });
    }
}

上面程序就是把富贵和旺财两人做的事情都封装到了 list 里面,然后遍历这个 list 把里面的东西,即“做的事情”都扔到线程池里面去。

那么上面的程序执行后,一种可能的输出是这样的:

image.png

乍一看没问题,富贵和旺财都在同时做事。

但是仔细一看,每个人做的事情的顺序不对了啊。

比如旺财看起来有点“精神分裂”,刚刚启动 Idea,嘴角就开始疯狂上扬了。

所以,到这里可以引出我想要的东西了。

我想要的是什么样的东西呢?

就是在保证富贵和旺财在同时做事的情况下,还要保证他们的做的事情是有一定顺序的,即按照我投放到线程池里面的顺序来执行。

用正式一点的话来描述是这样的:

我需要这样的一个线程池,它可以确保投递进来的任务按某个维度划分出任务,然后按照任务提交的顺序依次执行。这个线程池可以通过并行处理(多个线程)来提高吞吐量、又要保证一定范围内的任务按照严格的先后顺序来运行。

用我前面的例子,“按某个维度”就是人名,就是富贵和旺财这个维度。

请问你怎么做?




一顿分析


我会怎么做?

首先,我可以肯定的是 JDK 的线程池是干不成这个事儿的。

因为从线程池原理的角度来说,并行和先后顺序它是不能同时满足的。

你明白我意思吧?

比如我要用线程池来保证先后顺序,那么它是这样的:


image.png

只有一个线程的线程池,它可以保证先后顺序。

但是这玩意有意义吗?

有点意义,因为它并不占用主线程,但是意义不大,毕竟阉割了重要的“多线程”能力。

所以我们怎么在这个场景下把并行能力给提上去呢?

等等,我们好像已经有一个可以保证先后顺序的线程池了。

那么我们把它横向扩容,多搞几个,不就具备了并行的能力了吗?

image.png

然后前面提到的“按某个维度”,如果有多个只有一个线程的线程池了,那我也可以按照这个维度去映射“维度”和“每个线程池”呀。

用程序来说就是这样的:

image.png

标号为 ① 的地方就是搞了多个只有一个线程的线程池,目的是为了保证消费的顺序性。

标号为 ② 的地方就是通过一个 map 映射人名和线程池之间的关系。这里只是一个示意,比如我们还可以用用户号取模的方式去定位对应的线程池,比如用户号为奇数的用一个线程池,为偶数的用另外一个线程。

所以并不是“某个维度”里面有多少个数据就要定义多少个只有一个线程的线程池,它们也是可以复用的,这个地方有个小弯要转过来。

标号为 ③ 的地方就是根据名称去 map 里面去对应的线程池。

从输出结果来看,也是没有毛病的:

image.png

看到这里有的朋友就要说:你这不是作弊吗?

不是说好一个线程池吗,你这都弄了多个了。

你要这个角度看问题的话,那就把路走窄了。

你要想着有一个大的线程池,里面又放了很多个只有一个线程的线程池。

这样格局就打开了。

image.png

我上面的写法是一个非常简陋的 Demo,主要是引出这个方案的思路。

我要介绍的,就是基于这个思路搞出的一个开源项目。

是一位大公司的大佬写的,我看了一下源码,拍案叫绝:写的真他娘的好。

我先给你上一个使用案例和输出结果:

image.png

从案例看起来,使用方式也是非常的简单。

和 JDK 原生的用法的差异点就是我框起来的部分。

首先搞一个 KeyAffinityExecutor 的对象,来代替原生的线程池。

KeyAffinityExecutor 其中涉及到一个单词,Affinity。

翻译过来有类同的含义:

image.png

所以 KeyAffinityExecutor 翻译过来就是 key 类同的线程池,当你明白它的功能和作用范围后会觉得这个名字取的是针不戳。

接着是调用了 KeyAffinityExecutor 对象的 executeEx 方法,可以多传入一个参数,这个参数就是区分某一类相同任务的维度,比如我这里就给的是 name 字段。

从使用案例上看来,可以说封装的非常好,开箱即用。

image.png


目录
相关文章
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
2月前
|
消息中间件 前端开发 NoSQL
面试官:线程池遇到未处理的异常会崩溃吗?
面试官:线程池遇到未处理的异常会崩溃吗?
76 3
面试官:线程池遇到未处理的异常会崩溃吗?
|
2月前
|
消息中间件 存储 前端开发
面试官:说说停止线程池的执行流程?
面试官:说说停止线程池的执行流程?
51 2
面试官:说说停止线程池的执行流程?
|
2月前
|
消息中间件 前端开发 NoSQL
面试官:如何实现线程池任务编排?
面试官:如何实现线程池任务编排?
33 1
面试官:如何实现线程池任务编排?
|
3月前
|
Java
【多线程面试题二十五】、说说你对AQS的理解
这篇文章阐述了对Java中的AbstractQueuedSynchronizer(AQS)的理解,AQS是一个用于构建锁和其他同步组件的框架,它通过维护同步状态和FIFO等待队列,以及线程的阻塞与唤醒机制,来实现同步器的高效管理,并且可以通过实现特定的方法来自定义同步组件的行为。
【多线程面试题二十五】、说说你对AQS的理解
|
3月前
|
消息中间件 缓存 算法
Java多线程面试题总结(上)
进程和线程是操作系统管理程序执行的基本单位,二者有明显区别: 1. **定义与基本单位**:进程是资源分配的基本单位,拥有独立的内存空间;线程是调度和执行的基本单位,共享所属进程的资源。 2. **独立性与资源共享**:进程间相互独立,通信需显式机制;线程共享进程资源,通信更直接快捷。 3. **管理与调度**:进程管理复杂,线程管理更灵活。 4. **并发与并行**:进程并发执行,提高资源利用率;线程不仅并发还能并行执行,提升执行效率。 5. **健壮性**:进程更健壮,一个进程崩溃不影响其他进程;线程崩溃可能导致整个进程崩溃。
50 2
|
3月前
|
存储 缓存 安全
Java多线程面试题总结(中)
Java内存模型(JMM)定义了程序中所有变量的访问规则与范围,确保多线程环境下的数据一致性。JMM包含主内存与工作内存的概念,通过8种操作管理两者间的交互,确保原子性、可见性和有序性。`synchronized`和`volatile`关键字提供同步机制,前者确保互斥访问,后者保证变量更新的可见性。多线程操作涉及不同状态,如新建(NEW)、可运行(RUNNABLE)等,并可通过中断、等待和通知等机制协调线程活动。`volatile`虽不确保线程安全,但能确保变量更新对所有线程可见。
20 0
|
1月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
44 1
C++ 多线程之初识多线程
|
26天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
26天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2