并发编程十三:线程池任务类型和ForkJoin实战详解

线程池任务类型和ForkJoin实战详解

线程池任务类型

我们调整线程池中的线程数量的最主要的目的是为了充分并合理地使用CPU和内存等资源,从而最大限度地提高程序的性能。在实际工作中,我们需要根据任务类型的不同选择对应的策略。

CPU密集型任务
CPU密集型任务也叫计算密集型任务,比如加密、解密、压缩、计算等一系列需要大量耗 费 CPU 资源的任务。对于这样的任务最佳的线程数为 CPU 核心数的 1~2 倍,如果设置过多的线程数,实际上并不会起到很好的效果。此时假设我们设置的线程数量是 CPU核心数的2倍以 上,因为计算任务非常重,会占用大量的 CPU 资源,所以这时 CPU 的每个核心工作基本都是满负荷的,而我们又设置了过多的线程,每个线程都想去利用 CPU 资源来执行自己的任务,这 就会造成不必要的上下文切换,此时线程数的增多并没有让性能提升,反而由于线程数量过多 会导致性能下降。

IO密集型任务
IO密集型任务,比如数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗CPU资源,但是IO操作很耗时,总体会占用比较多的时间。对于这种任务最大线程数一般会大于 CPU 核心数很多倍,因为 IO 读写速度相比于CPU的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致CPU资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待IO的时候,它们此时并不需要CPU来计算,那么另外的线程便可以利用CPU去执行其他的任务,互不影响,这样的话在工作队列中等待的任务就会减少,可以更好地 利用资源。

有了以上的了解思考下如何充分利用多核CPU的性能,计算一个很大数组中所有整数的和?这涉及到分治的概念

分治算法

分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。
分治算法的步骤如下:

  1. 分解:将要解决的问题划分成若干规模较小的同类问题;
  2. 求解:当子问题划分得足够小时,用较简单的方法解决;
  3. 合并:按原问题的要求,将子问题的解逐层合并构成原问题的解。

在这里插入图片描述
在分治法中,子问题一般是相互独立的,因此,经常通过递归调用算法来求解子问题。
在这里插入图片描述
应用场景
分治思想在很多领域都有广泛的应用,例如算法领域有分治算法(归并排序、快速排序都 属于分治算法,二分法查找也是一种分治算法);大数据领域知名的计算框架 MapReduce 背后的思想也是分治。既然分治这种任务模型如此普遍,那Java显然也需要支持,Java 并发包 里提供了一种叫做 Fork/Join 的并行计算框架,就是用来支持分治这种任务模型的。

Fork/Join框架

介绍

传统线程池ThreadPoolExecutor有两个明显的缺点:

  • 一是无法对大任务进行拆分,对于某个任务只能由单线程执行;
  • 二是工作线程从队列中获取任务时存在竞争情况。

这两个缺点都会 影响任务的执行效率。为了解决传统线程池的缺陷,Java7中引入Fork/Join框架,并在Java8中 得到广泛应用。Fork/Join框架的核心是ForkJoinPool类,它是对 AbstractExecutorService类的扩展。ForkJoinPool允许其他线程向它提交任务,并根据设定将这些任务拆分为粒度更细的子任务,这些子任务将由ForkJoinPool内部的工作线程来并行执行,并且工作线程之间可以窃取彼此之间的任务。
在这里插入图片描述
ForkJoinPool最适合计算密集型任务,而且最好是非阻塞任务。ForkJoinPool是ThreadPoolExecutor线程池的一种补充,是对计算密集型场景的加强。
根据经验和实验,任务总数、单任务执行耗时以及并行数都会影响到Fork/Join的性能。所以,当你使用Fork/Join框架时,你需要谨慎评估这三个指标,最好能通过模拟对比评估,不要凭感觉冒然在生产环境使用。

Fork/Join使用

Fork/Join 计算框架主要包含两部分,一部分是分治任务的线程池 ForkJoinPool,另一部分是分治任务 ForkJoinTask。
ForkJoinPool
ForkJoinPool 是用于执行 ForkJoinTask 任务的执行池,不再是传统执行池 Worker+Queue 的组合式,而是维护了一个队列数组 WorkQueue(WorkQueue[]),这样在提交任务和线程任务的时候大幅度减少碰撞。
ForkJoinPool构造器
在这里插入图片描述
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()——提交任务
    fork()方法用于向当前任务所运行的线程池中提交任务。如果当前线程是ForkJoinWorkerThread类型,将会放入该线程的工作队列,否则放入common线程池的工作队列中。
  • join()——获取任务执行结果
    join()方法用于获取任务的执行结果。调用join()时,将阻塞当前线程直到对应的子任务完成运行并返回结果。

通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下三个子类:

  • RecursiveAction:用于递归执行但不需要返回结果的任务。
  • RecursiveTask :用于递归执行需要返回结果的任务。
  • CountedCompleter :在任务完成执行后会触发执行一个自定义的钩子函数

ForkJoinTask使用限制
ForkJoinTask最适合用于纯粹的计算任务,也就是纯函数计算,计算过程中的对象都是独立的,对外部没有依赖。提交到ForkJoinPool中的任务应避免执行阻塞I/O。

Fork/Join实战案例
下面对RecursiveAction、RecursiveTask 、CountedCompleter几种类型的任务实战案例

RecursiveAction类型
当我们的任务不需要返回值,比如很大的数组排序就可用到这种方式

import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

import static org.junit.Assert.assertArrayEquals;

public class SortTaskDemo {
	//内部类继承RecursiveAction 重写compute 方法
	static class SortTask extends RecursiveAction {
		final int[] array;
		final int lo, hi;
		static final int THRESHOLD = 1000;

		SortTask(int[] array, int lo, int hi) {
			this.array = array;
			this.lo = lo;
			this.hi = hi;
		}

		SortTask(int[] array) {
			this(array, 0, array.length);
		}

		protected void compute() {
			if (hi - lo < THRESHOLD)
				sortSequentially(lo, hi);
			else {
				int mid = (lo + hi) >>> 1;
				SortTask task1 = new SortTask(array, lo, mid);
				SortTask task2 = new SortTask(array, mid, hi);
				task1.fork();
				task2.fork();
				task1.join();
				task2.join();
				merge(lo, mid, hi);
			}
		}
		void sortSequentially(int lo, int hi) {
			Arrays.sort(array, lo, hi);
		}

		void merge(int lo, int mid, int hi) {
			int[] buf = Arrays.copyOfRange(array, lo, mid);
			for (int i = 0, j = lo, k = mid; i < buf.length; j++)
				array[j] = (k == hi || buf[i] < array[k]) ?
						buf[i++] : array[k++];
		}
	}

	public static void main(String[] args) {
		//构建数组
		int[] arrayToSort = Utils.buildRandomIntArray(20000);
		//复制上面的数组
		int[] expectedArray = Arrays.copyOf(arrayToSort, arrayToSort.length);
		//断言两个数组是否相同 因为是复制过来目前肯定相同
		assertArrayEquals(expectedArray, arrayToSort);
		//通过Arrays工具类进行排序
		Arrays.sort(expectedArray);
		int nofProcessors = Runtime.getRuntime().availableProcessors();
		ForkJoinPool forkJoinPool = new ForkJoinPool(nofProcessors);
		SortTaskDemo.SortTask task = new SortTask(arrayToSort);
		forkJoinPool.invoke(task);
		//断言 通过Arrays 排序的数组 和ForkJoinPool排序的数组是否相同
		assertArrayEquals(expectedArray, arrayToSort);
	}
}

RecursiveTask类型
当需要返回值的时候,例如计算很大的数组总和

public class LongSumDemo {
	//静态内部类 继承RecursiveTask 重写 compute
	static class LongSumTask extends RecursiveTask<Long> {
		// 任务拆分最小阈值
		static final int SEQUENTIAL_THRESHOLD = 10000000;
		int low,high;
		int[] array;
		LongSumTask(int[] arr, int low, int high) {
			this.array = arr;
			this.low = low;
			this.high = high;
		}
		LongSumTask(int[] arr) {
			this(arr,0,arr.length);
		}
		@Override
		protected Long compute() {
			if (high - low < SEQUENTIAL_THRESHOLD)
				return Sum(array,low,high);
			else {
				// 任务过大继续拆分
				int mid = low + (high - low) / 2;
				LongSumTask left = new LongSumTask(array, low, mid);
				LongSumTask right = new LongSumTask(array, mid, high);
				// 提交任务
				left.fork();
				right.fork();
				//获取任务的执行结果,将阻塞当前线程直到对应的子任务完成运行并返回结果
				long rightAns = right.join();
				long leftAns = left.join();
				return leftAns + rightAns;
			}
		}
		private Long Sum(int[] array,int low,int high){
			long sum = 0;
			for (int i = low; i < high; ++i) {
				sum += array[i];
			}
			return sum;
		}
	}
	public static void main(String[] args) {
		//准备数组
		int[] array = Utils.buildRandomIntArray(100000000);
		//单线程计算总和
		long seqSum = seqSum(array);
		System.out.println(seqSum);
		//创建forkjoin
		int nofProcessors = Runtime.getRuntime().availableProcessors();
		ForkJoinPool forkJoinPool = new ForkJoinPool(nofProcessors);
		//通过forkjoin 计算
//		Long invoke = forkJoinPool.invoke(new LongSumTask(array));
		ForkJoinTask<Long> submit = forkJoinPool.submit(new LongSumTask(array));
		Long invoke = submit.invoke();
		System.out.println(invoke);
	}

	private static long seqSum(int[] array) {
		long sum = 0;
		for (int i = 0; i < array.length; ++i) {
			sum += array[i];
		}
		return sum;
	}
}

CountedCompleter
求阶乘的例子

public class CountedCompleterExample {
    public static void main (String[] args) {
        List<BigInteger> list = new ArrayList<>();
        for (int i = 3; i < 20; i++) {
            list.add(new BigInteger(Integer.toString(i)));
        }
        ForkJoinPool.commonPool().invoke(
                            new FactorialTask(null, list));
    }
    /**
     * 阶乘
     */
    private static class FactorialTask extends CountedCompleter<Void> {
        //阈值
        private static int SEQUENTIAL_THRESHOLD = 5;
        private List<BigInteger> integerList;
        private int numberCalculated;
        private FactorialTask (CountedCompleter<Void> parent,
                            List<BigInteger> integerList) {
            super(parent);
            this.integerList = integerList;
        }
        @Override
        public void compute () {
            if (integerList.size() <= SEQUENTIAL_THRESHOLD) {
                showFactorial();
            } else {
                int middle = integerList.size() / 2;
                List<BigInteger> rightList = integerList.subList(middle,
                                    integerList.size());
                List<BigInteger> leftList = integerList.subList(0, middle);
                addToPendingCount(2);

                FactorialTask taskRight = new FactorialTask(this, rightList);
                FactorialTask taskLeft = new FactorialTask(this, leftList);

                taskLeft.fork();
                taskRight.fork();
            }
            tryComplete();
        }
        @Override
        public void onCompletion (CountedCompleter<?> caller) {
            if (caller == this) {
                System.out.printf("completed thread : %s numberCalculated=%s%n", Thread
                                    .currentThread().getName(), numberCalculated);
            }
        }
        private void showFactorial () {

            for (BigInteger i : integerList) {
                BigInteger factorial = CalcUtil.calculateFactorial(i);
                System.out.printf("%s! = %s, thread = %s%n", i, factorial, Thread
                                    .currentThread().getName());
                numberCalculated++;
            }
        }
    }
}

ForkJoin原理分析

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

工作窃取
ForkJoinPool与ThreadPoolExecutor有个很大的不同之处在于,ForkJoinPool存在引入了工作窃取设计,它是其性能保证的关键之一。工作窃取,就是允许空闲线程从繁忙线程的双端队列中窃取任务。默认情况下,工作线程从它自己的双端队列的头部获取任务。但是,当自己的任务为空时,线程会从其他繁忙线程双端队列的尾部中获取任务。这种方法,最大限度地减少了线程竞争任务的可能性。
ForkJoinPool的大部分操作都发生在工作窃取队列(work-stealing queues ) 中,该队列由内部类WorkQueue实现。它是Deques的特殊形式,但仅支持三种操作方式:push、pop和poll(也称为窃取)。在ForkJoinPool中,队列的读取有着严格的约束,push和pop仅能从其所属线程调用,而poll则可以从其他线程调用。
工作窃取的运行流程如下图所示 :
在这里插入图片描述

  • 工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争;
  • 工作窃取算法缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

这样做的主要原因是为了提高性能,通过始终选择最近提交的任务,可以增加资源仍分配在CPU缓存中的机会,这样CPU处理起来要快一些。而窃取者之所以从尾部获取任务,则是为了降低线程之间的竞争可能,毕竟大家都从一个部分拿任务,竞争的可能要大很多。
此外,这样的设计还有一种考虑。由于任务是可分割的,那队列中较旧的任务最有可能粒度较大,因为它们可能还没有被分割,而空闲的线程则相对更有“精力”来完成这些粒度较大的任务。

ForkJoin流程图
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
线程池Java中用于管理和复用线程的机制,它可以提高线程的利用率和性能。线程池的七大参数包括: 1. corePoolSize(核心线程数):线程池中始终保持的线程数量,即使它们处于空闲状态。当任务数量超过核心线程数时,线程池会创建新的线程来处理任务。 2. maximumPoolSize(最大线程数):线程池中允许存在的最大线程数量。当任务数量超过最大线程数时,新的任务会被放入等待队列中等待执行。 3. keepAliveTime(线程空闲时间):当线程池中的线程数量超过核心线程数时,多余的空闲线程在等待新任务到来时的最长等待时间。超过这个时间,空闲线程会被销毁。 4. unit(时间单位):用于设置keepAliveTime的时间单位,可以是秒、毫秒、分钟等。 5. workQueue(任务队列):用于存放等待执行的任务的队列。常见的队列类型有有界队列(如ArrayBlockingQueue)和无界队列(如LinkedBlockingQueue)。 6. threadFactory(线程工厂):用于创建新线程的工厂类。可以自定义线程的名称、优先级等属性。 7. handler(拒绝策略):当任务无法被线程池执行时的处理策略。常见的策略有直接抛出异常、丢弃任务、丢弃队列中最旧的任务等。 线程池的作用主要有以下几点: 1. 提高性能:线程池可以复用线程,避免了频繁创建和销毁线程的开销,提高了系统的性能。 2. 控制资源:通过设置核心线程数和最大线程数,可以控制系统中并发线程的数量,避免资源被过度占用。 3. 提供任务队列:线程池可以提供一个任务队列,用于存放等待执行的任务。当线程池中的线程都在执行任务时,新的任务会被放入队列中等待执行。 4. 管理线程:线程池可以统一管理线程的生命周期,包括创建、销毁、空闲时间等。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值