首页 > 编程语言 >[Java] 多线程系列之Fork/Join框架[转载]

[Java] 多线程系列之Fork/Join框架[转载]

时间:2023-02-11 14:57:17浏览次数:60  
标签:Fork Join 队列 任务 线程 end 多线程

1 工作原理

1.1 核心思想

Fork/Join框架是Java 7提供的一个用于并行执行任务的框架, 核心思想就是把大任务分割成若干个小任务最终汇总每个小任务结果后得到大任务结果,其实现思想与MapReduce有异曲同工之妙。

比如,计算1+2+…+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。

Fork/Join运行流程图如下:

1.2 工作窃取算法

Fork/Join框架使用一个巧妙的算法来平衡线程的负载,称为工作窃取(work-stealing)算法。工作窃取的运行流程图如下:

假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程队列一一对应,比如A线程负责处理A队列里的任务。
但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。
而在这时它们会访问同一个队列,所以为了减少窃取任务线程被窃取任务线程之间的竞争,通常会使用双端队列被窃取任务线程永远从双端队列头部拿任务执行,而窃取任务的线程永远从双端队列尾部拿任务执行。
工作窃取算法优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如:双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

1.3 内部构成

Fork/Join框架主要由两部分组成(分别使用2个类来实现):

1.3.1 分割任务(ForkJoinTask)

  • 分割任务。首先,我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。

我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行 fork()join()操作的机制。通常情况下,我们不需要直接继承ForkJoinTask类,而只需要继承其子类,Fork/Join框架提供了以下两个子类:

  • RecursiveAction:用于没有返回结果的任务。
  • RecursiveTask :用于有返回结果的任务。

1.3.2 执行任务并合并结果(ForkJoinPool)

  • 执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

ForkJoinPool 需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

1.4 源码解读【待完善】

1.4.1 ForkJoinPool

ForkJoinPool代表一个需要执行的任务,真正执行这些任务的线程放在一个里面,是一个可以执行的ExcuteService。但不同的是,它采用了work-stealing模式:所有在池中的空闲线程尝试去执行其他线程创建的子任务,这样就很少有线程处于空闲状态,非常高效。

池中维护着ForkJoinWorkerThread对象数组:

/**
* Array holding all worker threads in the pool. Initialized upon
* construction. Array size must be a power of two. Updates and
* replacements are protected by scanGuard, but the array is
* always kept in a consistent enough state to be randomly
* accessed without locking by workers performing work-stealing,
* as well as other traversal-based methods in this class, so long
* as reads memory-acquire by first reading ctl. All readers must
* tolerate that some array slots may be null.
*/
ForkJoinWorkerThread[] workers;

ForkJoinWorkerThread为任务的执行线程workers数组在构造方法中初始化,其大小必须为2的n次方(方便将取模转换为移位运算)。

ForkJoinPool初始化方法:

2 案例演示

案例1

让我们通过一个简单的需求来使用下Fork/Join框架

  • 需求:计算1~8的累加结果。

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

/**
 * @author johnny-zen
 * @create-time 2023/2/11 13:54
 * @description ...
 * @reference-doc
 *  [1] 好文推荐:JAVA基础之Fork/Join框架 - baidu - https://baijiahao.baidu.com/s?id=1709429670171703195
 */
public class CountTask extends RecursiveTask<Integer> {

    private static final int THRESHHOLD = 2;
    private int start;
    private int end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        System.out.println(start + " - " + end + " begin");
        int sum = 0;
        boolean canCompute = (end - start) <= THRESHHOLD;
        if (canCompute) { // 达到了计算条件,则直接执行
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else { // 不满足计算条件,则分割任务
            int middle = (start + end) / 2;

            CountTask leftTask = new CountTask(start, middle);
            CountTask rightTask = new CountTask(middle + 1, end);

            leftTask.fork(); // 执行子任务
            rightTask.fork();
            int leftResult = leftTask.join(); // 等待子任务执行完毕
            int rightResult = rightTask.join();

            sum = leftResult + rightResult; // 合并子任务的计算结果
        }
        System.out.println(start + + end + " end");
        return sum;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ForkJoinPool pool = new ForkJoinPool();
        CountTask task = new CountTask(1, 8);
        Future<Integer> future = pool.submit(task);
        //在执行的时候可能会抛出异常,但我们没办法在主线程里直接捕获异常,所以提供了 isCompletedAbnormally() 方法来检查任务是否已经抛出异常或已经被取消了,且可通过 getException方法 获取异常
        if (task.isCompletedAbnormally()) {//检查任务是否已经抛出异常或已经被取消了
            //方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回 null
            Throwable exception = task.getException();
            System.out.println(exception);
        } else {
            // 由于每个任务是由线程池执行的,每次的执行顺序会有不同,但是,父任务肯定在所有子任务之后完成
            // 比如1-8的计算肯定在子任务1-4、5-8之后完成,但是1-4、5-8的完成顺序是不确定的。
            System.out.println("result: " + future.get());//36
        }
    }
}

output

1 - 8 begin
5 - 8 begin
1 - 4 begin
1 - 2 begin
5 - 6 begin
3 end
11 end
3 - 4 begin
7 - 8 begin
15 end
13 end
7 end
5 end
9 end
result: 36

X 参考文献

标签:Fork,Join,队列,任务,线程,end,多线程
From: https://www.cnblogs.com/johnnyzen/p/17111654.html

相关文章

  • redis 单线程? 多线程?
    redis单线程?多线程?没有锁的竞争,没有线程的上下文切换。主要的性能瓶颈是内存或者网络带宽而非CPU1.redis3.x版本,也就是大家口口相传的redis是单线程2.redis4.x版本,严......
  • C++多线程
    语言级别的多线程,优点是跨平台底层仍然是调用系统API(识别不同系统,调用不同的系统调用)创建线程头文件threadthread的构造函数:thread()noexcept//构造线程对象,不执行......
  • Java多线程
    ​​Java多线程超详细!​​什么是线程?多线程?线程是一个程序内部的一条执行路径,我们之前启动程序执行后,main方法的执行其实就是一条单独的执行路径。多线程是指从软硬件上实......
  • Java 多线程编程
    Java多线程编程Java给多线程编程提供了内置的支持。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任......
  • 线程join 僵尸线程
    进程死亡后,由父进程负责回收PCB资源,不回收则会出现僵尸进程对于线程来说,任何一个线程都可以回收另一个线程的资源通常在主线程中通过pthread_join来回收子线程的资源调......
  • Mysql 的三种 join方式
    mysql只有三种join方式参考:https://www.jianshu.com/p/76c90b03b7bdmysql只有三种join方式,但通过组合可以达到各种效果而已join就是innerjoinleftjoinrigh......
  • ChatGPT: delphi 实现生产者消费者队列处理多线程示例
    usesClasses,SyncObjs;typeTItem=classData:Integer;end;TProducer=class(TThread)privateFQueue:TThreadedQueue<TItem>;protect......
  • Java多线程06——JUC并发包02
    1线程的同步工具类​​CountDownLatch​​​​CountDownLatch​​同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。​​CountDownLatch......
  • 如何使用 Python 编程进行多线程并发?
    当单线程python爬虫已经不能满足企业需求时,很多程序员会进行改代码或者增加服务器数量,这样虽说也能达到效果,但是对于人力物力也是一笔不小的消耗。如果是技术牛点的,正常都......
  • Practice2.多线程
    实验任务使用Pthreads库创建多个线程,并观察线程的并发执行现象以及数据共享关系MonteCarlo技术计算Π值(多线程):Π=4*(圆内点数)/(总的点数)1.随机的生成一个点Rando......