论文翻译 | 【深入挖掘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框架的本质和原理(线程盗取)

相关文章
|
11天前
|
XML Java 编译器
Java注解的底层源码剖析与技术认识
Java注解(Annotation)是Java 5引入的一种新特性,它提供了一种在代码中添加元数据(Metadata)的方式。注解本身并不是代码的一部分,它们不会直接影响代码的执行,但可以在编译、类加载和运行时被读取和处理。注解为开发者提供了一种以非侵入性的方式为代码提供额外信息的手段,这些信息可以用于生成文档、编译时检查、运行时处理等。
45 7
|
12天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
20 0
|
2天前
|
存储 监控 安全
单位网络监控软件:Java 技术驱动的高效网络监管体系构建
在数字化办公时代,构建基于Java技术的单位网络监控软件至关重要。该软件能精准监管单位网络活动,保障信息安全,提升工作效率。通过网络流量监测、访问控制及连接状态监控等模块,实现高效网络监管,确保网络稳定、安全、高效运行。
27 11
|
12天前
|
JavaScript 安全 Java
java版药品不良反应智能监测系统源码,采用SpringBoot、Vue、MySQL技术开发
基于B/S架构,采用Java、SpringBoot、Vue、MySQL等技术自主研发的ADR智能监测系统,适用于三甲医院,支持二次开发。该系统能自动监测全院患者药物不良反应,通过移动端和PC端实时反馈,提升用药安全。系统涵盖规则管理、监测报告、系统管理三大模块,确保精准、高效地处理ADR事件。
|
17天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
58 6
|
19天前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
38 0
|
3天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
26 6
|
18天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
16天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
18天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####