java线程池,工作窃取算法
前言
在上一篇《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,而有特定目的的则使用newWorkStealingPool
或 new 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线程池,工作窃取算法的更多相关文章
- Java线程池工作原理
前言 当项目中有频繁创建线程的场景时,往往会用到线程池来提高效率.所以,线程池在项目开发过程中的出场率是很高的. 那线程池是怎么工作的呢?它什么时候创建线程对象,如何保证线程安全... 什么时候创建线 ...
- Netty核心概念(7)之Java线程池
1.前言 本章本来要讲解Netty的线程模型的,但是由于其是基于Java线程池设计而封装的,所以我们先详细学习一下Java中的线程池的设计.之前也说过Netty5被放弃的原因之一就是forkjoin结 ...
- Java线程池原理解读
引言 引用自<阿里巴巴JAVA开发手册> [强制]线程资源必须通过线程池提供,不允许在应用中自行显式创建线程. 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销 ...
- java并发编程(十五)----(线程池)java线程池简介
好的软件设计不建议手动创建和销毁线程.线程的创建和销毁是非常耗 CPU 和内存的,因为这需要 JVM 和操作系统的参与.64位 JVM 默认线程栈是大小1 MB.这就是为什么说在请求频繁时为每个小的请 ...
- 池化技术之Java线程池
https://blog.csdn.net/jcj_2012/article/details/84906657 作用 线程池,通过复用线程来提升性能; 背景 线程是一个操作系统概念.操作系统负责这个 ...
- (转载)JAVA线程池管理
平时的开发中线程是个少不了的东西,比如tomcat里的servlet就是线程,没有线程我们如何提供多用户访问呢?不过很多刚开始接触线程的开发攻城师却在这个上面吃了不少苦头.怎么做一套简便的线程开发模式 ...
- Java线程池详解(二)
一.前言 在总结了线程池的一些原理及实现细节之后,产出了一篇文章:Java线程池详解(一),后面的(一)是在本文出现之后加上的,而本文就成了(二).因为在写完第一篇关于java线程池的文章之后,越发觉 ...
- java并发编程(十八)----(线程池)java线程池框架Fork-Join
还记得我们在初始介绍线程池的时候提到了Executor框架的体系,到现在为止我们只有一个没有介绍,与ThreadPoolExecutor一样继承与AbstractExecutorService的For ...
- Java并发指南12:深度解读 java 线程池设计思想及源码实现
深度解读 java 线程池设计思想及源码实现 转自 https://javadoop.com/2017/09/05/java-thread-pool/hmsr=toutiao.io&utm_ ...
随机推荐
- 基于Ajax的前后端分离
这种开发模式可以称为SPA (Single Page Application 单页面应用)时代. 这种模式下,前后端的分工非常清晰,前后端的关键协作点是 Ajax 接口.看起来是如此美妙,但回过头来看 ...
- IT技术人员的自我修养
1. 前言 在IT领域摸爬滚打多年,从一个普通程序员到技术主管,到技术经理,再到技术总监,踩过不少坑.加过不少班,也背过不少锅,在提升自身技术能力与管理能力的同时,也一直在思考,作为IT ...
- jQuery 解析 url 参数
应用场景: 三毛:我现在拿到一个 url 地址(https://www.google.com/search?dcr=&ei=5C&q=param),我现在要获取 location.se ...
- 程序员的长安十二时辰:Java实现从Google oauth2.0认证调用谷歌内部api
最近公司在做一个app购买的功能,主要思路就是客户在app上购买套餐以后,Google自动推送消息到Java后端,然后Java后端通过订单的token获取订单信息,保存到数据库. Java后端要获取订 ...
- [ PyQt入门教程 ] Qt Designer工具的使用
Qt Designer是PyQt程序UI界面的实现工具,Qt Designer工具使用简单,可以通过拖拽和点击完成复杂界面设计,并且设计完成的.ui程序可以转换成.py文件供python程序调用.本文 ...
- docker的基本安装和命令详解
docker的安装 yum install docker-io docker的启动 /bin/systemctl start docker.service docker查找镜像 docker sear ...
- Maven项目的打包发布到Nexus私服和服务器
1.编写pom文件如下: <build> <plugins> <plugin> <groupId>org.apache.maven.plugins< ...
- 关于Unity 中对UGUI制作任务系统的编程
版权声明: 本文原创发布于博客园"优梦创客"的博客空间(网址:http://www.cnblogs.com/raymondking123/)以及微信公众号"优梦创客&qu ...
- PyCharm如何导入python项目
Pycharm导入python项目 进入PyCharm后,点击File→Open,然后在弹窗中选择需要导入项目的文件夹: 打开了python项目后,需要配置该项目对应的python才可以正常运行: 配 ...
- 利用jQuery中的serialize方法大量获取页面中表单的数据,发送的服务器
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...