java线程池,工作窃取算法.md

简介: 在上一篇《java线程池,阿里为什么不允许使用Executors?》中我们谈及了线程池,同时又发现一个现象,当最大线程数还没有满的时候耗时的任务全部堆积给了单个线程那么有没有一种机制,在线程池中还有线程可以提供服务的时候帮忙分担一些已经被分配给某一个线程的耗时任务呢?

前言

在上一篇《java线程池,阿里为什么不允许使用Executors?》中我们谈及了线程池,同时又发现一个现象,当最大线程数还没有满的时候耗时的任务全部堆积给了单个线程, 代码如下:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
        1, //corePoolSize
        100, //maximumPoolSize
        100, //keepAliveTime
        TimeUnit.SECONDS, //unit
        new LinkedBlockingDeque<>(100));//workQueue

for (int i = 0; i < 5; i++) {
    final int taskIndex = i;
    executor.execute(() -> {
        System.out.println(taskIndex);
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}
// 输出: 0

下图很形象的说明了这个问题:

那么有没有一种机制,在线程池中还有线程可以提供服务的时候帮忙分担一些已经被分配给某一个线程的耗时任务呢?
答案当然是有的:工作窃取算法

工作窃取 (Work stealing)

这边大家先不要将这个跟java挂钩,因为这个属于算法,一种思想和套路,并不是特定语言特有的东西,所以不同的语言对应的实现也不尽一样,但核心思想一致。
这边会用“工作者”来代替线程的说法,如果在java中这个工作者就是线程。

工作窃取核心思想是,自己的活干完了去看看别人有没有没干完的活,如果有就拿过来帮他干。
大多数实现机制是:为每个工作者程分配一个双端队列(本地队列)用于存放需要执行的任务,当自己的队列没有数据的时候从其它工作者队列中获得一个任务继续执行。

我们来看一张图,这张图是发生了工作窃取时的状态。

可以看到工作者B的本地队列中没有了需要执行的规则,它正尝试从工作者A的任务队列中偷取一个任务。

为什么说尝试?因为涉及到并行编程肯定涉及到并发安全的问题,有可能在偷取过程中工作者A提前抢占了这个任务,那么B的偷取就会失败。大多数实现会尽量避免发生这个问题,所以大多数情况下不会发生。

并发安全的问题是怎么避免的呢?

一般是自己的本地队列采取LIFO(后进先出),偷取时采用FIFO(先进先出),一个从头开始执行,一个从尾部开始执行,由于偷取的动作十分快速,会大量降低这种冲突,也是一种优化方式。

Java中的工作窃取算法线程池

在Java 1.7新增了一个ForkJoinPool类,主要是实现了工作窃取算法的线程池,该类在1.8中被优化了,同时1.8在Executors类中还新增了两个newWorkStealingPool工厂方法。

java7中的fork/join task 和 java8中的并行stream都是基于ForkJoinPool。

// 使用当前处理器数, 相当于调用 newWorkStealingPool(Runtime.getRuntime().availableProcessors());
public static ExecutorService newWorkStealingPool();
public static ExecutorService newWorkStealingPool(int parallelism);

同时 ForkJoinPool 还在全局建立了一个公共的线程池

ForkJoinPool.commonPool();

默认的并行度是当前JVM识别到的处理器数。这个值也是可以通过参数进行变更的,下面是可以通过JVM熟悉进行commonPool设置的参数。

前缀统一为: java.util.concurrent.ForkJoinPool.common.
比如 parallelism 就要写为 java.util.concurrent.ForkJoinPool.common.parallelism

参数 描述 默认值
parallelism 并行级别 JVM识别到的处理器数
threadFactory 线程工厂类名 ForkJoinPool.DefaultForkJoinWorkerThreadFactory
exceptionHandler 错误处理程序 null
maximumSpares 最大允许额外线程数 256

使用工作窃取算法的线程池来优化之前的代码

ExecutorService executor = Executors.newWorkStealingPool(8);

for (int i = 0; i < 5; i++) {
    final int taskIndex = i;
    executor.execute(() -> {
        System.out.println(taskIndex);
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
}

// 无序输出 0~4

如果将Executors.newWorkStealingPool(8)改成ForkJoinPool.commonPool()会输出什么?

如果你能知道输出什么那么你对这个机制就算掌握了,会输出当前运行环境中处理器(cpu)数量的次数(如果核算大于5就只会输出5个结果)。

newWorkStealingPool 和 ForkJoinPool.commonPool 该优先选择哪个?

这个没有最优解,推荐执行的小任务(零散的)使用commonPool,而有特定目的的则使用newWorkStealingPoolnew ForkJoinPool

使用ForkJoinPool.commonPool 需要注意的问题

commonPool默认使用当前环境的处理器格式来当做并行程度,如果遇上堵塞形任务一样会遇到浪费算力的问题。
这点在容器化时需要特别注意,因为容器化的cpu个数限制往往不会太大。
这种时候可以通过设置默认的并行度或者使用newWorkStealingPool来手动指定并行度。

最后

为什么ForkJoinPool极少出现线程关键字?

现在许多语言淡化了线程这个概念,而golang中更是直接去掉了线程能力改为提供协程goroutine
目的还是线程是OS的资源,OS对程序内部运行其实并没有太了解,为了避免线程资源的浪费许多语言会自己管理线程。
对于程序来说我们关心的主要还是任务的并行运行,并不关心是线程还是协程。
下面是一些对应关系:

  • CPU : 线程 (1:N)
  • 线程 : 协程 (1:N)

CPU由OS管理,OS提供线程给程序使用,程序利用线程提供协程能力给应用使用。

ForkJoinPool一定更快吗?

不,大家都知道做的事情越多逻辑越复杂效率会越低。
ForkJoinPool中的工作队列,工作窃取都是需要额外管理的,同时也对线程调度和GC带来了压力。
所以ForkJoinPool并不是万能药大家根据具体需要去使用。

后面可能会跟大家分享下 Spring 中的 @Async

目录
相关文章
|
2月前
|
存储 算法 安全
探究‘公司禁用 U 盘’背后的哈希表算法与 Java 实现
在数字化办公时代,信息安全至关重要。许多公司采取“禁用U盘”策略,利用哈希表算法高效管理外接设备的接入权限。哈希表通过哈希函数将设备标识映射到数组索引,快速判断U盘是否授权。例如,公司预先将允许的U盘标识存入哈希表,新设备接入时迅速验证,未授权则禁止传输并报警。这有效防止恶意软件和数据泄露,保障企业信息安全。 代码示例展示了如何用Java实现简单的哈希表,模拟公司U盘管控场景。哈希表不仅用于设备管理,还在文件索引、用户权限等多方面助力信息安全防线的构建,为企业数字化进程保驾护航。
|
3月前
|
监控 算法 网络协议
Java 实现局域网电脑屏幕监控算法揭秘
在数字化办公环境中,局域网电脑屏幕监控至关重要。本文介绍用Java实现这一功能的算法,涵盖图像采集、数据传输和监控端显示三个关键环节。通过Java的AWT/Swing库和Robot类抓取屏幕图像,使用Socket进行TCP/IP通信传输图像数据,并利用ImageIO类在监控端展示图像。整个过程确保高效、实时和准确,为提升数字化管理提供了技术基础。
98 15
|
17天前
|
存储 监控 Java
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
142 60
【Java并发】【线程池】带你从0-1入门线程池
|
4月前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
136 2
|
13天前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
81 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
1月前
|
存储 算法 Java
解锁“分享文件”高效密码:探秘 Java 二叉搜索树算法
在信息爆炸的时代,文件分享至关重要。二叉搜索树(BST)以其高效的查找性能,为文件分享优化提供了新路径。本文聚焦Java环境下BST的应用,介绍其基础结构、实现示例及进阶优化。BST通过有序节点快速定位文件,结合自平衡树、多线程和权限管理,大幅提升文件分享效率与安全性。代码示例展示了文件插入与查找的基本操作,适用于大规模并发场景,确保分享过程流畅高效。掌握BST算法,助力文件分享创新发展。
|
2月前
|
存储 人工智能 算法
解锁分布式文件分享的 Java 一致性哈希算法密码
在数字化时代,文件分享成为信息传播与协同办公的关键环节。本文深入探讨基于Java的一致性哈希算法,该算法通过引入虚拟节点和环形哈希空间,解决了传统哈希算法在分布式存储中的“哈希雪崩”问题,确保文件分配稳定高效。文章还展示了Java实现代码,并展望了其在未来文件分享技术中的应用前景,如结合AI优化节点布局和区块链增强数据安全。
|
2月前
|
算法 安全 Java
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
103 16
|
2月前
|
运维 监控 算法
企业局域网监控软件中 Java 优先队列算法的核心优势
企业局域网监控软件是数字化时代企业网络安全与高效运营的基石,犹如一位洞察秋毫的卫士。通过Java实现的优先队列算法,它能依据事件优先级排序,确保关键网络事件如异常流量、数据泄露等被优先处理,保障系统稳定与安全。代码示例展示了如何定义网络事件类并使用PriorityQueue处理高优先级事件,尤其在面对疑似风险时迅速启动应急措施。这一核心技术助力企业在复杂网络环境中稳健前行,护航业务腾飞。
69 32
|
2月前
|
存储 监控 算法
剖析基于Java算法驱动的智能局域网管控之道
本文探讨了基于Java语言的局域网控制方案,结合链表数据结构与令牌桶算法,解决设备管理和流量调度难题。通过链表灵活存储网络设备信息,实现高效设备管理;令牌桶算法则精准控制流量,确保网络平稳运行。二者相辅相成,为校园、企业等局域网提供稳固高效的控制体系,保障业务连续性和数据安全。

热门文章

最新文章