论文翻译 | 【深入挖掘Java技术】「底层原理专题」深入分析一下并发编程之父Doug Lea的纽约州立大学的ForkJoin框架的本质和原理

简介: 本文深入探讨了一个Java框架的设计、实现及其性能。该框架遵循并行编程的理念,通过递归方式将问题分解为多个子任务,并利用工作窃取技术进行并行处理。所有子任务完成后,其结果被整合以形成完整的并行程序。在总体设计上,该框架借鉴了Cilk工作窃取框架的核心理念。其核心技术主要聚焦于高效的任务队列构建和管理,以及工作线程的管理。经过实际性能测试,我们发现大多数程序的并行加速效果显著,但仍有优化空间,未来可能需要进一步研究改进方案。

前提介绍

Doug Lea在州立大学奥斯威戈分校(Doug Lea)

摘要

本文深入探讨了一个Java框架的设计、实现及其性能。该框架遵循并行编程的理念,通过递归方式将问题分解为多个子任务,并利用工作窃取技术进行并行处理。所有子任务完成后,其结果被整合以形成完整的并行程序。

在总体设计上,该框架借鉴了Cilk工作窃取框架的核心理念。其核心技术主要聚焦于高效的任务队列构建和管理,以及工作线程的管理。经过实际性能测试,我们发现大多数程序的并行加速效果显著,但仍有优化空间,未来可能需要进一步研究改进方案。


引言

Fork/Join并行是一种简单而高效的设计技术。它的算法思想是分而治之算法的并行版本,其典型形式包括:首先将问题分解为两个或更多的子问题,然后对每个子问题进行独立求解,最后将各个子问题的解合并以形成最终的解决方案。

Result solve(Problem problem) {
   
   
     if (problem is small) 
         directly solve problem
     else {
   
   
         split problem into independent parts
         fork new subtasks to solve each part
         join all subtasks
         compose result from subresults
     }
}
  • fork操作会启动一个新的并行fork/join子任务。
  • join连接操作会导致当前任务不继续执行,直到子任务完成。

fork/join算法与其他一样,fork/join算法几乎总是递归的、反复拆分子任务,直到它们小到可以用简单、简短的顺序方法解决为止。使用简单、简短的顺序方法。

FJTask是支持这种编程风格的JavaTM框架。FJTask 作为java.util.concurrent包的一部分,可从 http://gee.cs.oswego.edu 获取。

设计

任何支持构建并行执行的子任务的框架来运行fork/join程序。支持构建并行执行的子任务、的框架运行。

不过,java.lang.Thread类(以及 POSIX pthreads 通常是 Java 线程的基础)不是支持 fork/join 程序的最优的工具。

性能优秀

fork/join任务的同步和管理要求相对简单和有规律。其产生的计算图允许采用不同于通用线程所需的调度策略。例如,除了等待子任务外,fork/join 任务从不需要阻塞。因此,通用线程的阻塞状态跟踪被视为一种资源浪费。

此外,fork/join 框架还可以利用工作窃取技术,将任务从繁忙的线程转移到空闲线程,进一步优化并行处理。

任务粒度合理

在基本任务粒度合理的情况下,构建和管理线程的成本可能高于任务本身的计算时间。虽然粒度可以在特定平台上运行程序时进行调整,但极粗粒度会限制利用并行性的机会。

简而言之,标准的线程框架过于复杂,无法满足大多数分叉/连接程序的需求。然而,线程作为其他类型并行和并行编程方式的基础,要仅仅为了支持这种编程风格而消除其开销或调整线程本身的调度是不可能的,或者至少是不切实际的。

Cilk框架和基础

虽然这些想法肯定有更长的历史,但第一个为这些问题提供系统解决方案的编程框架是Cilk。Cilk和其他轻量级可执行框架是在操作系统的基本线程或进程机制之上的特殊目的的框架,支持fork/join。

fork/join的可移植性

这种策略同样适用于Java,尽管Java线程又依赖于更低级别的操作系统功能。创建这样一个Java轻量级执行框架的主要优点是允许fork/join程序以更可移植的方式编写,并在各种支持JVM的系统上运行。

FJTask框架

FJTask框架是基于Cilk中使用的设 计的一个变体。其他变体存在于 Hood, Filaments,stackthreads,以及一些相关的轻量级系统中。

class ATask extends FJTask {
   
   
     public void run() {
   
   
         split...
         fork...
         join...
         compose...
 }
}

可执行任务。所有这些框架都将任务映射到线程,其方式与操作系统将线程映射到CPU相同,但在执行映射时,fork/join框架利用了fork/join程序的简单性、规律性和约束。虽然所有这些框架都可以适应(在不同程度上)以不同风格编写的并行程序,但它们针对fork/join设计进行了优化。

设计思路

线程映射关系

已经建立了一个工作线程池。每个工作线程都是一个标准的(“重的”)线程(这里是线程子类FJTaskRunner的一个实例),它负责处理队列中保存的任务。通常,系统上的工作线程数量和CPU核心数一样多。在Cilk等本地框架中,这些线程被映射到内核线程或轻量级进程,然后再映射到CPU。

在Java中,必须信任JVM和OS才能将这些线程映射到CPU。然而,对于操作系统来说,这是一个相对简单的任务,因为这些线程是计算密集型的。任何合理的映射策略都会将这些线程映射到不同的CPU核心上。

拆分子任务

在FJTask框架中,所有的fork/join任务都是轻量级可执行类的实例,而不是线程的实例。这些任务子类化FJTask,而不是线程,因为独立的可执行任务需要实现接口Runnable并定义一个run方法。

此外,这些任务都实现了Runnable接口,这使得它们可以作为正在执行的任务或线程的一部分交替运行。由于任务在FJTask方法支持的受限制的规则下操作,因此对FJTask进行子类化更加方便,以便能够直接调用它们。

排队及调度

在特殊目的的排队和调度规则下,任务通过工作线程得以执行和管理。这些机制通过任务类中的方法触发,主要包括fork、join、完成状态指示器isDone,以及一些实用的方法,如coInvoke,即分叉并随后连接两个或多个任务。

设置调度管理

一个简单的控制和管理工具(这里是FJTaskRunnerGroup)在从普通线程(如在Java程序中执行主任务的线程)调用时,设置工作池并启动给定的分叉/连接任务的执行。

标准示例

作为程序员如何看待这个框架的标准示例,这里是一个计算斐波那契函数的类。

 static final int threshold = 13; 
 volatile int number; // arg/result
     Fib(int n) {
   
    number = n; }
     int getAnswer() {
   
   
         if (!isDone()) 
            throw new IllegalStateException();
             return number;
    }
 public void run() {
   
   
     int n = number;
     if (n <= threshold) // granularity ctl
         number = seqFib(n);
     else {
   
   
         Fib f1 = new Fib(n − 1);
         Fib f2 = new Fib(n − 2);
         coInvoke(f1, f2); 
         number = f1.number + f2.number;
     }
 }
 public static void main(String[] args) {
   
   
     try {
   
   
         int groupSize = 2; // for example 
         FJTaskRunnerGroup group = new FJTaskRunnerGroup(groupSize);
         Fib f = new Fib(35); // for example
         group.invoke(f);
         int result = f.getAnswer();
         System.out.println("Answer: " +result);
     }catch (InterruptedException ex) {
   
   } 
     }
     int seqFib(int n) {
   
   
         if (n <= 1) return n;
             else return seqFib(n−1) + seqFib(n−2);
     }
}

这个版本的运行速度至少比在一个新的java.lang中运行的同等程序快30倍。它在维护多线程Java程序的内在可移植性的同时也做到了这一点。程序员典型感兴趣的调优参数:

  • 在构建工作线程时,其数量通常应与平台上的可用CPU数量相匹配(或更少,以保留处理用于其他非相关目的),有时甚至可能更多,以吸收非计算任务。

  • 一个粒度参数用于确定何时生成任务的成本超过了潜在的并行性带来的好处。这个参数更多地依赖于算法本身,而不是平台。通常,我们可以设定一个阈值,当在单处理器上运行时能获得良好的结果,但当存在多个CPU时仍能充分利用它们。这种方法的好处在于它与JVM的动态编译机制相契合,能够更优化地处理小方法。此外,数据局部性的优势也使得fork/join算法在某些情况下优于其他类型的算法。

未完待续

本节内容,给大家带来了对应的fork/join框架的前世今生,以及基于框架的fork和join机制的论文介绍,后续接下来会给大家带来对应的【线程盗取篇章】:论文翻译 | 【深入挖掘Java技术】「底层原理专题」深入分析一下并发编程之父Doug Lea的纽约州立大学的ForkJoin框架的本质和原理(线程盗取)

相关文章
|
2月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
235 1
|
2月前
|
存储 Java Go
【Java】(3)8种基本数据类型的分析、数据类型转换规则、转义字符的列举
牢记类型转换规则在脑海中将编译和运行两个阶段分开,这是两个不同的阶段,不要弄混!
217 2
|
3月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
3月前
|
安全 Java API
Java Web 在线商城项目最新技术实操指南帮助开发者高效完成商城项目开发
本项目基于Spring Boot 3.2与Vue 3构建现代化在线商城,涵盖技术选型、核心功能实现、安全控制与容器化部署,助开发者掌握最新Java Web全栈开发实践。
446 1
|
3月前
|
安全 Cloud Native Java
Java 模块化系统(JPMS)技术详解与实践指南
本文档全面介绍 Java 平台模块系统(JPMS)的核心概念、架构设计和实践应用。作为 Java 9 引入的最重要特性之一,JPMS 为 Java 应用程序提供了强大的模块化支持,解决了长期存在的 JAR 地狱问题,并改善了应用的安全性和可维护性。本文将深入探讨模块声明、模块路径、访问控制、服务绑定等核心机制,帮助开发者构建更加健壮和可维护的 Java 应用。
311 0
|
3月前
|
监控 Cloud Native Java
Quarkus 云原生Java框架技术详解与实践指南
本文档全面介绍 Quarkus 框架的核心概念、架构特性和实践应用。作为新一代的云原生 Java 框架,Quarkus 旨在为 OpenJDK HotSpot 和 GraalVM 量身定制,显著提升 Java 在容器化环境中的运行效率。本文将深入探讨其响应式编程模型、原生编译能力、扩展机制以及与微服务架构的深度集成,帮助开发者构建高效、轻量的云原生应用。
452 44
|
2月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
202 1
|
2月前
|
JSON 网络协议 安全
【Java基础】(1)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
226 1
Java 数据库 Spring
182 0
|
3月前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
296 16