前言

在上一篇《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. ASP.NET--Web服务器端控件和Html控件

    今天学习总结了一些相关概念和知识. 之前无论是做 单机的winform 还是 CS的winform 感觉,不到两年下来感觉还可以,虽然API有很多,但是还是比较熟悉基于WINDOWS消息机制的编程,但 ...

  2. 微信小程序 setData 数组 渲染问题 删除之后的数组渲染不正确

    list: [ { id: , mode: , src: ' }, { id: , mode: , src: ' }, { id: , mode: , src: ' } ], onDelete(e) ...

  3. 简单的量子算法(一):Hadamard 变换、Parity Problem

    Hadamard Transform Hadamard 变换在量子逻辑门中提过,只不过那时是单量子的Hadamard门,负责把\(|1\rangle\)变成\(|-\rangle\),\(|0\ran ...

  4. [小米OJ] 8. 最少交换次数

    求逆序对数即可 function solution(line) { var nums = line.split(","); var res = 0; for (let i = 0; ...

  5. Netty中的责任链模式

    适用场景: 对于一个请求来说,如果有个对象都有机会处理它,而且不明确到底是哪个对象会处理请求时,我们可以考虑使用责任链模式实现它,让请求从链的头部往后移动,直到链上的一个节点成功处理了它为止 优点: ...

  6. IO流1

    一.I/0:input/output1.java.io.file表示:文件或文件夹(目录)File f = new File("文件路径");注意:相对路径:非web项目的相对都是 ...

  7. 12. 集合类Collection和Map

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

  8. vuex快速入门

    vuex是什么? vuex是一个专门为vue.js应用程序开发的状态管理模式. vuex默认的五种基本的对象: state:存储状态(可以理解为变量)可以从计算属性中返回某个状态 getters:通常 ...

  9. Linux系统管理----磁盘管理与文件系统

    1.为主机新增两块30GB的SCSI硬盘 找到要添加的虚拟机,单击鼠标右键,点击设置 点击添加 选择硬件类型,然后点击下一步 选择要创建的磁盘类型,然后点击下一步 指定要创建磁盘的容量,然后点击下一步 ...

  10. WPF滑块控件(Slider)的自定义样式

    前言 每次开发滑块控件的样式都要花很久去读样式代码,感觉有点记不牢,所以特此备忘. 自定义滑块样式 首先创建项目,添加Slider控件. 然后获取Slider的Window样式,如下图操作. 然后弹出 ...