Java并发之执行器 ——Fork-Join框架

前言

Fork-Join将一个大任务分解成多个子任务,分别进行处理,最后再将处理结果合并。

双端队列与工作密取(Work Stealing)

Fork-Join用到的模式 -工作密取

Java6 增加了两种容器类型,Deque (发 音 为 “deck”)和 BlockingDeque, 它们分别对 Queue和 BlockingQueue进行了扩展。Deque是一个双端队列,实现了在队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和 LinkedBlockingDeque。

正如阻塞队列适用于生产者-消费者模式,双端队列同样适用于另一种相关模式,即工作密取 (Work Stealing)。在生产者-消费者设计中,所有消费者有一个共享的工作队列,而在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中的全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。密取工作模式比传统的生 产者-消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发 生竞争。在大多数时候,它们都只是访问自己的双端队列,从而极大地减少了竞争。当工作者 线程需要访问另一个队列时,它会从队列的尾部而不是从头部获取工作,因此进一步降低了队列上的竞争程度。

工作密取非常适用于既是消费者也是生产者问题— 当执行某个工作时可能导致出现更多 的工作。例如,在网页爬虫程序中处理一个页面时,通常会发现有更多的页面需要处理。类似的还有许多搜索图的算法,例如在垃圾回收阶段对堆进行标记,都可以通过工作密取机制来实 现髙效并行。当一个工作线程找到新的任务单元时,它会将其放到自己队列的末尾(或者在工作共享设计模式中,放入其他工作者线程的队列中)。当双端队列为空时,它会在另一个线程的队列队尾查找新的任务,从而确保每个线程都保持忙碌状态。

Fork-Join介绍

Fork-Join使用工作密取模式来完成并行任务执行。如果是我们来设计算法应该如何?

1.分割任务。把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小

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

Fork/Join使用两个类来完成以上两件事情:

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

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

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

Fork-Join使用

需求:向数组装入一千万次随机数生成结果,并统计其中大于0.5的个数

public class ForkJoinTest {
	
	public static void main(String[] args) {
		final int SIZE = 10000000;//一千万次随机
		double[] numbers = new double[SIZE];
		for(int i =0;i<SIZE;i++) numbers[i] = Math.random();
		Counter counter = new ForkJoinTest().new Counter(numbers, 0, numbers.length, x -> x > 0.5); //
		ForkJoinPool pool = new ForkJoinPool();
		pool.invoke(counter);
		System.out.println(counter.join()); //5001344
		
		
	}
	
	
	class Counter extends RecursiveTask<Integer>{
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		public static final int THRESHOLD = 1000;//阀门值 分割次数
		private double[] values;
		private int from;
		private int to;
		private DoublePredicate filter; //?
		
		public Counter(double[] values, int from, int to, DoublePredicate filter){
			this.values = values;
			this.from = from;
			this.to = to;
			this.filter = filter;
		}
		
		@Override
		protected Integer compute() {
			if(to - from < THRESHOLD){
				int count = 0;
				for(int i = from;i<to;i++){
					if(filter.test(values[i])) count++;
				}
				return count;	
			}else{
				int mid = (from + to)/2;
				Counter first = new Counter(values,from,mid,filter);
				Counter second = new Counter(values,mid,to,filter);
				invokeAll(first,second);//接受任务并阻塞
				return first.join() + second.join();//生成结果
			}
			
		}
				
		
	}

}

源码

看看ForkJoinTask的fork方法,向ForkJoinWorkerThread(当前线程)的workQueue(支持工作窃取和外部任务提交的队列)放入任务

public final ForkJoinTask<V> fork() {
        Thread t;
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }

ForkJoinPool.WorkQueue的push方法,ForkJoinPool的signalWork()方法唤醒或创建一个工作线程来执行任务

final void push(ForkJoinTask<?> task) {
            ForkJoinTask<?>[] a; ForkJoinPool p;
            int b = base, s = top, n;
            if ((a = array) != null) {    // ignore if queue removed
                int m = a.length - 1;     // fenced write for task visibility
                U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
                U.putOrderedInt(this, QTOP, s + 1);
                if ((n = s - b) <= 1) {
                    if ((p = pool) != null)
                        p.signalWork(p.workQueues, this);
                }
                else if (n >= m)
                    growArray();
            }
        }

ForkJoinTask的join方法。join方法的主要作用是阻塞当前线程并等待获取结果。使用doJoin()方法执行,并方法处理结果状态

public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }

doJoin()方法 。在doJoin()方法里,首先通过查看任务的状态,看任务是否已经执行完了,如果执行完了,则直接返回任务状态,如果没有执行完,则从任务数组里取出任务并执行。如果任务顺利执行完成了,则设置任务状态为NORMAL,如果出现异常,则纪录异常,并将任务状态设置为EXCEPTIONAL。

private int doJoin() {
        int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
        return (s = status) < 0 ? s :
            ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
            (w = (wt = (ForkJoinWorkerThread)t).workQueue).
            tryUnpush(this) && (s = doExec()) < 0 ? s :
            wt.pool.awaitJoin(w, this, 0L) :
            externalAwaitDone();
    }

小结

java原生fork-join框架运用上不普遍但给我们提供了一种并行计算的思路

猜你喜欢

转载自blog.csdn.net/zl_momomo/article/details/81531974