前言

在上一篇《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

java线程池,工作窃取算法的更多相关文章

  1. Java线程池工作原理

    前言 当项目中有频繁创建线程的场景时,往往会用到线程池来提高效率.所以,线程池在项目开发过程中的出场率是很高的. 那线程池是怎么工作的呢?它什么时候创建线程对象,如何保证线程安全... 什么时候创建线 ...

  2. Netty核心概念(7)之Java线程池

    1.前言 本章本来要讲解Netty的线程模型的,但是由于其是基于Java线程池设计而封装的,所以我们先详细学习一下Java中的线程池的设计.之前也说过Netty5被放弃的原因之一就是forkjoin结 ...

  3. Java线程池原理解读

    引言 引用自<阿里巴巴JAVA开发手册> [强制]线程资源必须通过线程池提供,不允许在应用中自行显式创建线程. 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销 ...

  4. java并发编程(十五)----(线程池)java线程池简介

    好的软件设计不建议手动创建和销毁线程.线程的创建和销毁是非常耗 CPU 和内存的,因为这需要 JVM 和操作系统的参与.64位 JVM 默认线程栈是大小1 MB.这就是为什么说在请求频繁时为每个小的请 ...

  5. 池化技术之Java线程池

     https://blog.csdn.net/jcj_2012/article/details/84906657 作用 线程池,通过复用线程来提升性能; 背景 线程是一个操作系统概念.操作系统负责这个 ...

  6. (转载)JAVA线程池管理

    平时的开发中线程是个少不了的东西,比如tomcat里的servlet就是线程,没有线程我们如何提供多用户访问呢?不过很多刚开始接触线程的开发攻城师却在这个上面吃了不少苦头.怎么做一套简便的线程开发模式 ...

  7. Java线程池详解(二)

    一.前言 在总结了线程池的一些原理及实现细节之后,产出了一篇文章:Java线程池详解(一),后面的(一)是在本文出现之后加上的,而本文就成了(二).因为在写完第一篇关于java线程池的文章之后,越发觉 ...

  8. java并发编程(十八)----(线程池)java线程池框架Fork-Join

    还记得我们在初始介绍线程池的时候提到了Executor框架的体系,到现在为止我们只有一个没有介绍,与ThreadPoolExecutor一样继承与AbstractExecutorService的For ...

  9. Java并发指南12:深度解读 java 线程池设计思想及源码实现

    ​深度解读 java 线程池设计思想及源码实现 转自 https://javadoop.com/2017/09/05/java-thread-pool/hmsr=toutiao.io&utm_ ...

随机推荐

  1. (CVE-2017-10271)weblogic12.1.3.0漏洞测试与打补丁过程

    1.漏洞测试 搭建完成weblogic12.1.3.0后,开始用工具测试 点击connect,右下角显示connected,说明已连接→说明漏洞存在 CMD输入:ls   ,然后点击Execute执行 ...

  2. Java+eclipse技巧小总结

    首先是打开Content Assistant,自动代码补全 Window -> Preferences -> Java -> Editor -> Content Assist, ...

  3. [PTA] 数据结构与算法题目集 6-7 在一个数组中实现两个堆栈

    //如果堆栈已满,Push函数必须输出"Stack Full"并且返回false:如果某堆栈是空的,则Pop函数必须输出"Stack Tag Empty"(其中 ...

  4. typedef int a[10];怎么解释?

    typedef int a[10]; a b[10]; 为什么分配400个字节的空间? int a[10];为什么分配了40个字节的空间? 问题:应该怎么解释typedef的这种行为呢?而如果换成是# ...

  5. 算法-一步步教你如何用c语言实现堆排序(非递归)

    看了左神的堆排序,觉得思路很清晰,比常见的递归的堆排序要更容易理解,所以自己整理了一下笔记,带大家一步步实现堆排序算法 首先介绍什么是大根堆:每一个子树的最大值都是子树的头结点,即根结点是所有结点的最 ...

  6. java练习---11

    package cn.lyhh; class Person{ private String name; private int age; static String city = "A城&q ...

  7. java用最少循环求两个数组的交集、差集、并集

    import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List ...

  8. 恢复在iterm2中当滚动光标时候触发滚动历史记录的问题

    在Iterm2中,如果你上下滚动光标(上下滑动触摸板.或者滚动鼠标滚轮),通常情况下是触发了屏幕内容上下滚动. 但是在某些异常情况下,却触发了命令行历史记录的上下滚动,效果和你连续按了多次键盘的上下键 ...

  9. .NET Core CSharp初级篇 类的生命历程

    .NET Core CSharp初级篇 1-7 本节内容为类的生命周期 引言 对象究竟是一个什么东西?对于许多初学者而言,对象都是一个非常抽象的知识点.如果非要用一句话描述,我觉得"万物皆对 ...

  10. 【未解决】iOS QBImagePickerController访问相册没有取消和确定按钮

    这两天调程序时遇到了这个问题,如图所示: 感觉这问题也是奇葩………… 用系统的 UIImagePickerController 替换后就正常了.看来是 QBImagePickerController ...