Java并发编程 | 第六篇:Fork/Join框架

Fork/Join框架的介绍

Fork/Join框架是Java7提供了的一个用于并发执行任务的框架,是一个实现了ExecutorService接口的多线程处理器。它可以把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架,充分利用可用的资源,进而提高应用的执行效率

Fork/Join执行逻辑
这里写图片描述

使用Fork/Join框架

第一步首先分割任务,需要一个fork类来把大任务分割成小任务,如果子任务还是很大,那继续分割直到足够小
第二步然后合并任务结果,分割子任务分别放在双端队列,然后几个启动线程分别从双端队列中获取任务执行,子任务执行完的结果统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

常用方法

  1. compute(); 计算方法(分拆的子任务)
  2. fork(); // 执行子任务
  3. join(); // 子任务结束后返回对应结果

import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;

public class CountTask extends RecursiveTask<Long> {
	private static final int THRESHOLD = 10000;//任务分解的规模

	private long start;
	private long end;

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

	public Long compute() {
		long sum = 0;
		//如果需要求和的总数大于THRESHOLD,那么任务继续分解,否则直接可以执行了
		boolean canCompute = (end - start) < THRESHOLD;
		if (canCompute) {
			for (long i = start; i < end; i++) {
				sum += i;
			}
		} else {
			// 分成100任务
			long step = (start + end) / 100;
			ArrayList<CountTask> subTasks = new ArrayList<CountTask>();
			long pos = start;
			for (int i = 0; i < 100; i++) {
				long lastOne = pos + step;
				if (lastOne > end)
					lastOne = end;
				CountTask subTask = new CountTask(pos, lastOne);
				pos += step + 1;
				subTasks.add(subTask);
				subTask.fork();
			}
			for (CountTask t : subTasks) {
				sum += t.join();
			}
		}
		return sum;
	}

	public static void main(String[] args) {
	//创建一个ForkJoinPool线程池
		ForkJoinPool forkJoinPool = new ForkJoinPool();
		//构造一个任务
		CountTask task = new CountTask(0, 200000L);
		//提交线程池后拿到结果的任务
		ForkJoinTask<Long> result = forkJoinPool.submit(task);
		try {
		//从任务获取结果
			long res = result.get();
			System.out.println("sum=" + res);
		} catch (Exception e) {

		}
	}

}

使用ForkJoin框架经常使用到两个任务模型RecursiveTask和RecursiveAction,RecursiveTask用于定义有返回值的任务,RecursiveAction用于定义没有返回值的任务,首先建立ForkJoinPool线程池,再构造一个计算1到200000求和的任务。将任务提交给线程池,线程池会返回一个携带结果的任务,通过get()获取最终结果,如果调用get()时候任务还没完成就等待。

如果任务的划分层次过深会出现两种情况:
1、系统内的线程数量越积越多,导致性能下降
2、函数调用层次很深,导致栈溢出

工作窃取算法

工作窃取算法是指某个线程从其他队列中窃取任务来执行
在一般情况下,一个物理线程实际需要处理多个逻辑任务,因此每个线程必然需要拥有一个任务队列。比如,线程A已经把自己所以任务都执行完了,而线程B还有一堆任务没有完成,那么线程A就会帮助线程B,从线程B中拿一些任务过来处理,尽可能达到平衡,总是从任务队列的底部拿数据,而线程执行自己的任务是从相反的顶部开始拿。这样就有利于避免数据竞争

我们在实现 分治编程时,主要就是调用 ForkJoinTask 的 fork() 和 join() 方法。fork() 方法用于提交子任务,而 join() 方法则用于等待子任务的完成。而这个过程中,将涉及到 “工作窃取算法”。

1、 fork( ) 方法提交任务

public final ForkJoinTask<V> fork() {
        Thread t;
        //判断是否是一个 工作线程
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            //加入到内部队列中
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else//由 common 线程池来执行任务
            ForkJoinPool.common.externalPush(this);
        return this;
    }

fork()方法先判断当前线程(调用fork()来提交任务的线程)是不是一个 ForkJoinWorkerThread 的工作线程,如果是,则将任务加入到内部队列中,否则,由 ForkJoinPool 提供的内部公用的线程池 common 线程池 来执行这个任务。

根据上面的说法,意味着我们可以在普通线程池中直接调用 fork() 方法来提交任务到一个默认提供的线程池中。这将非常方便。假如,你要在程序中处理大任务,需要分治编程,但你仅仅只处理一次,以后就不会用到,而且任务不算太大,不需要设置特定的参数,那么你肯定不想为此创建一个线程池,这时默认的提供的线程池将会很有用。

2、join( ) 等待任务的完成

public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();//直接返回结果
    }
 private int doJoin() {
        int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        return 
            //如果完成,直接返回s
            (s = status) < 0 ? s : 
            //没有完成,判断是不是池中的 ForkJoinWorkerThread 工作线程
            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            //如果是池中线程,执行这里
            (w = (wt = (ForkJoinWorkerThread)t).workQueue).
            tryUnpush(this) && (s = doExec()) < 0 ? s :
            wt.pool.awaitJoin(w, this, 0L) :
            //如果不是池中的线程池,则执行这里
            externalAwaitDone();
    }

仔细看上面的注释。当 dojoin( )方法发现任务没有完成且当前线程是池中线程时,执行了 tryUnpush( )方法。tryUnpush()方法尝试去执行此任务:如果要join的任务正好在当前任务队列的顶端,那么pop出这个任务,然后调用 doExec() 让当前线程去执行这个任务。

final boolean tryUnpush(ForkJoinTask<?> t) {
            ForkJoinTask<?>[] a; int s;
            if ((a = array) != null && (s = top) != base &&
                U.compareAndSwapObject
                (a, (((a.length - 1) & --s) << ASHIFT) + ABASE, t, null)) {
                U.putOrderedInt(this, QTOP, s);
                return true;
            }
            return false;
        }
 final int doExec() {
        int s; boolean completed;
        if ((s = status) >= 0) {
            try {
                completed = exec();
            } catch (Throwable rex) {
                return setExceptionalCompletion(rex);
            }
            if (completed)
                s = setCompletion(NORMAL);
        }
        return s;
    }

如果任务不是处于队列的顶端,那么就会执行 awaitJoin( ) 方法。

   final int awaitJoin(WorkQueue w, ForkJoinTask<?> task, long deadline) {
        int s = 0;
        if (task != null && w != null) {
            ForkJoinTask<?> prevJoin = w.currentJoin;
            U.putOrderedObject(w, QCURRENTJOIN, task);
            CountedCompleter<?> cc = (task instanceof CountedCompleter) ?
                (CountedCompleter<?>)task : null;
            for (;;) {
                if ((s = task.status) < 0)//如果任务完成了,跳出死循环
                    break;
                if (cc != null)//当前任务是CountedCompleter类型,则尝试从任务队列中获取当前任务的派生子任务来执行;
                    helpComplete(w, cc, 0);
                else if (w.base == w.top || w.tryRemoveAndExec(task))//如果当前线程的内部队列为空,或者成功完成了任务,帮助某个线程完成任务。
                    helpStealer(w, task);
                if ((s = task.status) < 0)//任务完成,跳出死循环
                    break;
                long ms, ns;
                if (deadline == 0L)
                    ms = 0L;
                else if ((ns = deadline - System.nanoTime()) <= 0L)
                    break;
                else if ((ms = TimeUnit.NANOSECONDS.toMillis(ns)) <= 0L)
                    ms = 1L;
                if (tryCompensate(w)) {
                    task.internalWait(ms);
                    U.getAndAddLong(this, CTL, AC_UNIT);
                }
            }
            U.putOrderedObject(w, QCURRENTJOIN, prevJoin);
        }
        return s;
    }

重点说一下helpStealer。helpStealer的原则是你帮助我执行任务,我也帮你执行任务。

  1. 遍历奇数下标,如果发现队列对象currentSteal放置的刚好是自己要找的任务,则说明自己的任务被该队列A的owner线程偷来执行
  2. 如果队列A队列中有任务,则从队尾(base)取出执行;
  3. 如果发现队列A队列为空,则根据它正在join的任务,在拓扑找到相关的队列B去偷取任务执行。在执行的过程中要注意,我们应该完整的把任务完成

它们可能会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务线程永远从双端队列的尾部拿任务执行。

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

参考:《Java高并发程序设计》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值