【并发编程】计算密集型场景的工具:Fork/Join入门

Fork/Join是什么

  • 传统线程池ThreadPoolExecutor有两个明显的缺点:一是无法对大任务进行拆分,对于某个任务只能由单线程执行;二是工作线程从队列中获取任务时存在竞争情况。
  • 这两个缺点都会影响任务的执行效率。为了解决传统线程池的缺陷,Java7中引入Fork/Join框架,并在Java8中得到广泛应用。
  • ForkJoinPool是ThreadPoolExecutor线程池的一种补充,是对计算密集型场景的加强。
  • Fork/Join框架的核心是ForkJoinPool类,它是对AbstractExecutorService类的扩展。
  • ForkJoinPool允许其他线程向它提交任务,并根据设定将这些任务拆分为粒度更细的子任务,这些子任务将由ForkJoinPool内部的工作线程来并行执行,并且工作线程之间可以窃取彼此之间的任务。

Fork/Join的适用场景

  • 最适合计算密集型任务。
  • 最好是非阻塞任务。
  • 任务总数、单任务执行耗时以及并行数都会影响到Fork/Join的性能。

Fork/Join的内容

  • 分治任务的线程池:ForkJoinPool
  • 分治任务:ForkJoinTask

分治任务的线程池:ForkJoinPool

  • ForkJoinPool 是用于执行 ForkJoinTask 任务的执行池,不再是传统执行池 Worker+Queue 的组合式,而是维护了一个队列数组 WorkQueue(WorkQueue[]),这样在提交任务和线程任务的时候大幅度减少碰撞。
  • ForkJoinPool中的任务执行可以去窃取其他线程的任务。
ForkJoinPool的构造方法

ForkJoinPool构造器.png

  • ForkJoinPool中有四个核心参数,用于控制线程池的并行数、工作线程的创建、异常处理和模式指定等。各参数解释如下:
  • int parallelism:指定并行级别(parallelism level)。ForkJoinPool将根据这个设定,决定工作线程的数量。如果未设置的话,将使用Runtime.getRuntime().availableProcessors()来设置并行级别;
  • ForkJoinWorkerThreadFactory factory:ForkJoinPool在创建线程时,会通过factory来创建。注意,这里需要实现的是ForkJoinWorkerThreadFactory,而不是ThreadFactory。如果你不指定factory,那么将由默认的DefaultForkJoinWorkerThreadFactory负责线程的创建工作;
  • UncaughtExceptionHandler handler:指定异常处理器,当任务在运行中出错时,将由设定的处理器处理;
  • boolean asyncMode:设置队列的工作模式:asyncMode ? FIFO_QUEUE : LIFO_QUEUE。当asyncMode为true时,将使用先进先出队列,而为false时则使用后进先出的模式。
ForkJoinPool的提交任务方式
  • execute方法:在提交任务后,不会返回结果。ForkJoinPool不仅允许提交ForkJoinTask类型任务,还允许提交Runnable任务。执行Runnable类型任务时,将会转换为ForkJoinTask类型。由于任务是不可切分的,所以这类任务无法获得任务拆分这方面的效益,不过仍然可以获得任务窃取带来的好处和性能提升。
  • invoke方法:接受ForkJoinTask类型的任务,并在任务执行结束后,返回泛型结果。如果提交的任务是null,将抛出空指针异常。
  • submit方法:支持三种类型的任务提交:ForkJoinTask类型、Callable类型和Runnable类型。在提交任务后,将返回ForkJoinTask类型的结果。如果提交的任务是null,将抛出空指针异常,并且当任务不能按计划执行的话,将抛出任务拒绝异常。

分治任务:ForkJoinTask

  • ForkJoinTask是ForkJoinPool的核心之一,它是任务的实际载体,定义了任务执行时的具体逻辑和拆分逻辑。
  • ForkJoinTask继承了Future接口,所以也可以将其看作是轻量级的Future。
  • ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,承载着主要的任务协调作用,一个用于任务提交,一个用于结果获取。
  • 提交任务fork():用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。
  • 获取任务执行结果join():用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。
ForkJoinTask的子类
  • 通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下三个子类:
  • RecursiveAction:用于递归执行但不需要返回结果的任务。
  • RecursiveTask:用于递归执行需要返回结果的任务。
  • CountedCompleter :在任务完成执行后会触发执行一个自定义的钩子函数

Fork/Join的实现原理

  • ForkJoinPool内部有多个工作队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个工作队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的工作队列中。
  • ForkJoinPool 的每个工作线程都维护着一个工作队列(WorkQueue),这是一个双端队列(Deque),里面存放的对象是任务(ForkJoinTask)。
  • 每个工作线程在运行中产生新的任务(通常是因为调用了 fork())时,会放入工作队列的top,并且工作线程在处理自己的工作队列时,使用的是 LIFO 方式,也就是说每次从top取出任务来执行。
  • 每个工作线程在处理自己的工作队列同时,会尝试窃取一个任务,窃取的任务位于其他线程的工作队列的base,也就是说工作线程在窃取其他工作线程的任务时,使用的是FIFO 方式。
  • 在遇到 join() 时,如果需要 join 的任务尚未完成,则会先处理其他任务,并等待其完成。
  • 在既没有自己的任务,也没有可以窃取的任务时,进入休眠 。

Fork/Join的使用方式

import java.time.Duration;
import java.time.Instant;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.IntStream;

public class ForkJoinTest {

	// 获取逻辑处理器数量,我的电脑是8
	static final int NCPU = Runtime.getRuntime().availableProcessors();

	// 计算的总数
	static long calcSum;

	public static void main(String[] args) throws Exception {
		// 准备数组
		int[] array = buildRandomIntArray(200000000);

		// 单线程程序的求和
		System.out.println("=============单线程任务开始执行=============");
		Instant now = Instant.now();
		calcSum = seqSum(array);
		System.out.println("执行的结果为:" + calcSum + "    执行时间为:" + Duration.between(now, Instant.now()).toMillis());
		System.out.println("=============单线程任务结束执行=============");
		System.out.println("\n");

		// 定义递归任务
		LongSum ls = new LongSum(array, 0, array.length);
		// 构建ForkJoinPool
		ForkJoinPool fjp = new ForkJoinPool(NCPU);

		// ForkJoin计算数组总和
		System.out.println("=============forkjoin任务开始执行=============");
		now = Instant.now();
		ForkJoinTask<Long> result = fjp.submit(ls);
		System.out.println("执行的结果为:" + result.get() + "    执行时间为:" + Duration.between(now, Instant.now()).toMillis());
		System.out.println("=============forkjoin任务结束执行=============");
		System.out.println("\n");
		fjp.shutdown();

		// 并行流计算数组总和
		System.out.println("=============并行流计算任务开始执行=============");
		now = Instant.now();
		Long sum = (Long) IntStream.of(array).asLongStream().parallel().sum();
		System.out.println("执行的结果为:" + sum + "    执行时间为:" + Duration.between(now, Instant.now()).toMillis());
		System.out.println("=============并行流计算任务结束执行=============");

	}

	// 数组元素相加
	static long seqSum(int[] array) {
		long sum = 0;
		for (int i = 0; i < array.length; ++i) {
			sum += array[i];
		}
		return sum;
	}

	// 数据初始化,得到一个长度为size的随机数数组
	public static int[] buildRandomIntArray(final int size) {
		int[] resultArray = new int[size];
		Random generator = new Random();
		for (int i = 0; i < resultArray.length; i++) {
			resultArray[i] = generator.nextInt(1000);
		}
		return resultArray;
	}
}

/**
 * 递归任务
 */
class LongSum extends RecursiveTask<Long> {
	// 任务拆分最小阈值
	static final int SEQUENTIAL_THRESHOLD = 100000000;

	// 开始计算的下标
	int low;
	// 结束计算的下标
	int high;
	// 计算的实际数组
	int[] array;

	LongSum(int[] arr, int lo, int hi) {
		array = arr;
		low = lo;
		high = hi;
	}

	@Override
	protected Long compute() {

		// 当任务拆分到小于等于阀值时开始求和
		if (high - low <= SEQUENTIAL_THRESHOLD) {
			long sum = 0;
			for (int i = low; i < high; ++i) {
				sum += array[i];
			}
			return sum;
		} else { // 任务过大继续拆分
			int mid = low + (high - low) / 2;
			LongSum left = new LongSum(array, low, mid);
			LongSum right = new LongSum(array, mid, high);
			// 提交任务
			left.fork();
			right.fork();
			// 获取任务的执行结果,将阻塞当前线程直到对应的子任务完成运行并返回结果
			long rightAns = right.compute();
			long leftAns = left.join();
			// 返回拆分后相加的结果
			return leftAns + rightAns;
		}
	}
}
  • 执行结果,稍微提高了一点点性能。

Fork/Join的运行结果.png

结束语

  • 获取更多本文的前置知识文章,以及新的有价值的文章,让我们一起成为架构师!
  • 关注公众号,可以让你对MySQL有非常深入的了解
  • 关注公众号,每天持续高效的了解并发编程!
  • 关注公众号,后续持续高效的了解spring源码!
  • 这个公众号,无广告!!!每日更新!!!
    作者公众号.jpg
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值