ForkJoin框架使用和原理剖析

一:ForkJoin理解

       人们认为,在此之前,人们或多或少对ForkJoin框架有了明确的理解,实际上,ForkJoin框架是执行服务的一个功能组合,执行任务,在其他线程中不执行的,通过分配工作算法(工作-stealing)。可充分发挥机器多处理器的优势,利用游离线并行快速完成大任务,它可以分为小任务,类似于算法的划分。ForkJoin的目标是利用所有可用的处理能力来提高程序的速度和性能。本文将介绍ForkJoin框架,介绍主要特点、用例,源代码分析和结果。
       默认情况下,并行线程数量等于可用处理器的数量ForkJoinPool与其他类型的ExecutorService的区别主要在于它使用了工作窃取:池中的所有线程都试图查找和执行提交给池的任务和/或其他活动任务创建的任务(如果不存在工作,则最终阻塞等待工作)。

ForkJoin框架使用和原理剖析_第1张图片

ForkJoin框架的核心是ForkJoinPool类,基于AbstractExecutorService扩展。ForkJoinPool中维护了一个队列数组WorkQueue[],每个WorkQueue维护一个ForkJoinTask数组和当前工作线程。ForkJoinPool实现了工作窃取(work-stealing)算法并执行ForkJoinTask。ForkJoinTask是能够在ForkJoinPool中执行的任务抽象类,父类是Future,具体实现类有很多,这里主要关注RecursiveAction和RecursiveTask。RecursiveAction是没有返回结果的任务,RecursiveTask是需要返回结果的任务。只需要实现其compute()方法,在compute()中做最小任务控制,任务分解(fork)和结果合并(join)。ForkJoinPool中执行的默认线程是ForkJoinWorkerThread,由默认工厂产生,可以自己重写要实现的工作线程。同时会将ForkJoinPool引用放在每个工作线程中,供工作窃取时使用。

二.变量的介绍

ForkJoinPool类

ADD_WORKER: 100000000000000000000000000000000000000000000000 -> 1000 0000 0000 0000,用来配合ctl在控制线程数量时使用

ctl: 控制ForkJoinPool创建线程数量,(ctl & ADD_WORKER) != 0L 时创建线程,也就是当ctl的第16位不为0时,可以继续创建线程

defaultForkJoinWorkerThreadFactory: 默认线程工厂,默认实现是DefaultForkJoinWorkerThreadFactory

runState: 全局锁控制,全局运行状态

workQueues: 工作队列数组WorkQueue[]

config: 记录并行数量和ForkJoinPool的模式(异步或同步)

ForkJoinTask类

status: 任务的状态,对其他工作线程和pool可见,运行正常则status为负数,异常情况为正数
WorkQueue类

qlock: 并发控制,put任务时的锁控制

array: 任务数组ForkJoinTask[]

pool: ForkJoinPool,所有线程和WorkQueue共享,用于工作窃取、任务状态和工作状态同步

base: array数组中取任务的下标

top: array数组中放置任务的下标

ForkJoinWorkerThread类

pool: ForkJoinPool,所有线程和WorkQueue共享,用于工作窃取、任务状态和工作状态同步workQueue: 当前线程的任务队列,与WorkQueue的owner呼应
 

ForkJoin框架使用和原理剖析_第2张图片

如图所示,不同类别和变量之间的关系:ForkJoinPool作为主要组件之一,支持所有WorkQueues作业队列,WorkKeues支持在执行工作流窃取算法的池中的所有线程线程。每个WorkQueue对象都使用pool保存对forkjoinpool的引用,以访问其工作Keues以窃取其他工作流。同时,WorkQueue对象中的owner是连接ForkJoinWorkThread和WorkQueue的ForkJoinWorkThread工作流,每个工作流都将优先执行队列中分配的任务,当队列中的任务是空的时,使用算法从其他任务的队列中获取任务,这些算法是由任务窃取的。forkjointask<>[]array,这是每个特定的任务,第一个插入array的任务是最大的。

简单的案例使用:

在这个阶段,大概对它的构成和工作窃取的方式有一个共同的认识,但对于通用的fork()和join()方法还是不清楚。下面是一个示例案例代码,让大家深入了解。

这里使用的是Red Forkjoin case,1-100位数字来求和,提高需求和效率。

    public class CountRecursiveTask extends RecursiveTask {
    	  //达到子任务直接计算的阈值
        private int Th = 15;

        private int start;
        private int end;

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

        @Override
        protected Integer compute() {
            if (this.end - this.start < Th) {
            		//如果小于阈值,直接调用最小任务的计算方法
                return count();
            } else {
                //fork 2 tasks:Th = 15
                //如果仍大于阈值,则继续拆分为2个子任务,分别调用fork方法。
                //这里可以根据情况拆成n个子任务
                int middle = (end + start) / 2;
                CountRecursiveTask left = new CountRecursiveTask(start, middle);
                System.out.println("start:" + start + ";middle:" + middle + ";end:" + end);
                left.fork();
                CountRecursiveTask right = new CountRecursiveTask(middle + 1, end);
                right.fork();
                //最后一定要记得fork()结果(如果需要结果的话)
                return left.join() + right.join();
            }
        }

        private int count() {
            int sum = 0;
            for (int i = start; i <= end; i++) {
                sum += i;
            }
            return sum;
        }
    }

    @Test
    public void testForkJoin() {
        ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());

        Integer sum = forkJoinPool.invoke(new CountRecursiveTask(1, 100));
        System.out.println(sum);
    }
    

可以自己运行找一下感觉,体验分而治之之美!

源码分析:

创建ForkJoinPoll的源码:

public ForkJoinPool(int parallelism,
                        ForkJoinWorkerThreadFactory factory,
                        UncaughtExceptionHandler handler,
                        boolean asyncMode) {
        this(checkParallelism(parallelism),
             checkFactory(factory),
             handler,
             asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
             "ForkJoinPool-" + nextPoolId() + "-worker-");
        checkPermission();
    }

ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());先看ForkJoinPool的创建过程,这个比较简单,创建了一个ForkJoinPool对象,带有默认ForkJoinWorkerThreadFactory,并行数跟机器核数一样,同步模式。

提交任务

forkJoinPool.invoke(new CountRecursiveTask(1, 100));会先执行到ForkJoinPool#externalPush中,此时forkJoinPool.workQueues并没有完成初始化工作,所以执行到ForkJoinPool#externalSubmit。

externalSubmit

这里是一个for无限循环实现,跳出逻辑全部在内部控制,主要结合runState来控制。

1、创建ForkJoinPool的WorkQueue[]变量workQueues,长度为大于等于2倍并行数量的且是2的n次幂的数。这里对传入的并行数量使用了位运算,来计算出workQueues的长度。

2、创建一个WorkQueue变量q,q.base=q.top=4096,q的owner为null,无工作线程,放入workQueues数组中

3、创建q.array对象,长度8192,将ForkJoinTask也就是代码案例中的CountRecursiveTask放入q.array,pool为传入的ForkJoinPool,并将q.top加1,完成后q.base=4096,q.top=4097。然后执行ForkJoinPool#signalWork方法。(base下标表示用来取数据的,top下标表示用来放数据的,当base小于top时,说明有数据可以取)

externalSubmit主要完成3个小步骤工作,每个步骤都使用了锁的机制来处理并发事件,既有对runState使用ForkJoinPool的全局锁,也有对WorkQueue使用局部锁。
 

ForkJoin框架使用和原理剖析_第3张图片

signalWork 

        signalWork方法的签名是:void signalWork(WorkQueue[] ws, WorkQueue q)。ws为ForkJoinPool中的workQueues,q为externalSubmit方法中新建的用于存放ForkJoinTask的WorkQueue.

         Signal Work将确定是否需要创建当前不存在的CTL参数的工作线程,因此转到TryaddWorker(),然后在CreateWorker()中创建它们,使用forkjoinworkerthread\\\35DM forkjoinworkerthread(forkjoinpool)的默认工厂方法创建forkjoinworkerthread,forkjoinpool创建pool。并为其所有者创建一个工作线程,该线程为空,并命名为forkjoinpool-1-worker-1,并将其放入pool数组中。工作。创建工作线程后开始()。因此,在Pool中创建了两个WorkQueue存储库。Workeues,其中一个保留了第一个大型forkjointask,owner numll,base=4096,top=4097;第二个魁北克所有者工作线程,array是空的,没有数据,base=4096,top=4096。

ForkJoin框架使用和原理剖析_第4张图片

ForkJoinWorkerThread#run

执行ForkJoinWorkerThread线程ForkJoinPool-1-worker-1,执行点来到ForkJoinWorkerThread#run,注意这里是在ForkJoinWorkerThread中,此时的workQueue.array还是空的,pool为文中唯一的一个,是各个线程会共享的。

run方法中首先是一个判断 if (workQueue.array == null) { // only run once,这也验证了我们前面的分析,当前线程的workQueue.array是空的。每个新建的线程,拥有的workQueue.array是没有任务的。那么它要执行的任务从哪里来?

runWorker()方法中会执行一个死循环,去scan扫描是否有任务可以执行。全文的讲到的工作窃取work-stealing算法,就在java.util.concurrent.ForkJoinPool#scan。当有了上图的模型概念时,这个方法的实现看过就会觉得其实非常简单。这里拿源码直接讲解
 

ForkJoin框架使用和原理剖析_第5张图片

	WorkQueue q; ForkJoinTask[] a; ForkJoinTask t;
	int b, n; long c;
	//如果pool.workQueues即ws的k下标元素不为空
	if ((q = ws[k]) != null) {
		//如果base)
	                  U.getObjectVolatile(a, i))) != null &&
	            q.base == b) {
	            if (ss >= 0) {
	            		//拿到任务之后,将array中的任务用CAS的方式置为null,并将base加1
	                if (U.compareAndSwapObject(a, i, t, null)) {
	                    q.base = b + 1;
	                    if (n < -1)       // signal others
	                        signalWork(ws, q);
	                    return t;
	                }
	            }
	            else if (oldSum == 0 &&   // try to activate
	                     w.scanState < 0)
	                tryRelease(c = ctl, ws[m & (int)c], AC_UNIT);
	        }
	        if (ss < 0)                   // refresh
	            ss = w.scanState;
	        r ^= r << 1; r ^= r >>> 3; r ^= r << 10;
	        origin = k = r & m;           // move and rescan
	        oldSum = checkSum = 0;
	        continue;
	    }
	    checkSum += b;
	}

疑问如下;

为什么在在pool.workQueues中拿到某个下标的WorkQueue对象没有使用getObjectVolatile这种方式获取?这也是Doug Lea厉害之处,这里用了更细粒度锁,让并发只在一个WorkQueue中,而不是整个workQueues中。从pool.workQueues中获得任务后,就会在ForkJoinPool.WorkQueue#runTask去执行.runTask方法中会依次执行到重写的RecursiveTask#compute方法中。

CountRecursiveTask#compute

重写此方法的规则:

if(任务足够小){
  直接执行任务;
  如果有结果,return结果;
}else{
  拆分为2个子任务;
  分别执行子任务的fork方法;
  执行子任务的join方法;
  如果有结果,return合并结果;
}

ForkJoinTask#fork 

ForkJoin框架使用和原理剖析_第6张图片

 public final ForkJoinTask fork() {
        Thread t;
        //如果是工作线程,则往自己线程中的workQuerue中添加子任务;否则走首次添加逻辑
        if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
            ((ForkJoinWorkerThread)t).workQueue.push(this);
        else
            ForkJoinPool.common.externalPush(this);
        return this;
    }
ForkJoinPool.WorkQueue#push方法会将当前子任务存放到array中,并调用ForkJoinPool#signalWork添加线程或等待其他线程去窃取任务执行。过程又回到前面讲到的signalWork流程。

ForkJoinTask#externalAwaitDone

主线程在把任务放置在第一个WorkQueue的array之后,启动工作线程就退出了。如果使用的是异步的方式,则使用Future的方式来获取结果,即提交的ForkJoinTask,通过isDone(),get()方法判断和得到结果。异步的方式跟同步方式在防止任务的过程是一样的,只是主线程可以任意时刻再通过ForkJoinTask去跟踪结果。本案例用的是同步的写法,因此主线程最后在ForkJoinTask#externalAwaitDone等待任务完成。这里主线程会执行Object#wait(long),使用的是Object类中的wait,在当前ForkJoinTask等待,直到被notify。而notify这个动作会在ForkJoinTask#setCompletion中进行,这里使用的是notifyAll,因为需要通知的有主线程和工作线程,他们都共同享用这个对象,需要被唤起

ForkJoinTask#join

来看left.join() + right.join(),在将left和right的Task放置在当前工作线程的workQueue之后,执行join()方法,join()方法最终会在ForkJoinPool.WorkQueue#tryRemoveAndExec中将刚放入的left取出,将对应workQueue中array的left任务置为空,然后执行left任务。然后执行到left的compute方法。对于right任务也是一样,继续子任务的fork和join工作,如此循环往复。
    public final V join() {
        int s;
        if ((s = doJoin() & DONE_MASK) != NORMAL)
            reportException(s);
        return getRawResult();
    }
当工作线程执行结束后,会执行getRawResult,拿到结果。

ForkJoin框架使用和原理剖析_第7张图片

因此,假设在整个项目执行过程中只会有一个并发数,即当left时,ForkJoinPool最多会有一个线程。Join将在左边分配所有任务,完成后执行,然后在右边分配任务并执行它们,这将是低效率的。因此,ForkJoin框架适合这种用途,并且为了能够结合任务执行,并行活动的数量必须超过两个。

如上图所示,根据我们的案例代码,ForkJoinPool中的线程将按每个任务进行分配或计数,并最终获得结果。此任务图是一个古老的图表,但在执行时,ForkJoinPool不会严格遵循在level树上执行的任务层次结构:例如计算机(1-13)可以先是计算机(26-50)或计算机(51-100)或计算机(76-100)。但是,对于树中的每个节点,必须在执行子任务之前执行父任务,否则可能会执行计算机(26-50)等子任务,必须在计算机(26-38)完成之前完成。

任务从上到下分配到可计算任务的最小值,而每个父节点使用left.join()+right.join()方法,负责汇总两个子任务/节点的结果,并最终得到计算机(1-100)的结果。与分割算法类似,它比分割算法略高,因为可以同时执行多个任务。

其中的高亮操作:

Work-Steal算法

相比其他线程池实现,这个是ForkJoin框架中最大的亮点。当空闲线程在自己的WorkQueue没有任务可做的时候,会去遍历其他的WorkQueue,并进行任务窃取和执行,提高程序响应和性能。
取2的n次幂作为长度的实现

    //代码位于java.util.concurrent.ForkJoinPool#externalSubmit
    if ((rs & STARTED) == 0) {
        U.compareAndSwapObject(this, STEALCOUNTER, null,
                               new AtomicLong());
        // create workQueues array with size a power of two
        int p = config & SMASK; // ensure at least 2 slots
        int n = (p > 1) ? p - 1 : 1;
        n |= n >>> 1; n |= n >>> 2;  n |= n >>> 4;
        n |= n >>> 8; n |= n >>> 16; n = (n + 1) << 1;
        workQueues = new WorkQueue[n];
        ns = STARTED;
    }

这里的p其实就是设置的并行线程数,在为ForkJoinPool创建WorkQueue[]数组时,会对传入的p进行一系列位运算,最终得到一个大于等于2p的2的n次幂的数组长度

内存屏障

    //代码位于java.util.concurrent.ForkJoinPool#externalSubmit
    if ((a != null && a.length > s + 1 - q.base) ||
        (a = q.growArray()) != null) {
        int j = (((a.length - 1) & s) << ASHIFT) + ABASE;
        //通过Unsafe进行内存值的设置,高效,且屏蔽了处理器和Java编译器的指令乱序问题
        U.putOrderedObject(a, j, task);
        U.putOrderedInt(q, QTOP, s + 1);
        submitted = true;
    }

这里在对单个WorkQueue的array进行push任务操作时,先后使用了putOrderedObject和putOrderedInt,确保程序执行的先后顺序,同时这种直接操作内存地址的方式也会更加高效。
高并发:细粒度WorkQueue的锁

    //代码位于java.util.concurrent.ForkJoinPool#externalSubmit
    //如果qlock为0,说明当前没有其他线程操作改WorkQueue
    //尝试CAS操作,修改qlock为1,对这个WorkQueue进行加锁
    if (q.qlock == 0 && U.compareAndSwapInt(q, QLOCK, 0, 1)) {
        ForkJoinTask[] a = q.array;
        int s = q.top;
        boolean submitted = false; // initial submission or resizing
        try {                      // locked version of push
            if ((a != null && a.length > s + 1 - q.base) ||
                (a = q.growArray()) != null) {
                int j = (((a.length - 1) & s) << ASHIFT) + ABASE;
                U.putOrderedObject(a, j, task);
                U.putOrderedInt(q, QTOP, s + 1);
                submitted = true;
            }
        } finally {
              //finally将qlock置为0,进行锁的释放,其他线程可以使用
            U.compareAndSwapInt(q, QLOCK, 1, 0);
        }
        if (submitted) {
            signalWork(ws, q);
            return;
        }
    }

这里对单个WorkQueue的array进行push任务操作时,使用了qlock的CAS细粒度锁,让并发只落在一个WOrkQueue中,而不是整个pool中,极大提高了程序的并发性能,类似于ConcurrentHashMap。

总结

主要的类:

1、RecursiveTask:

1)  RecursiveTask:表示一个“任务”,重写他的T  compute()方法,计算返回值;

2)  RecursiveTask有个fork()方法,表示启动、分发;

3)  RecursiveTask有个T  join()方法,表示等待、同步等待结果;

2、ForkJoinPool:

1)new ForkJoinPool(int):int表示启动线程数,默认为CPU的线程数;

2)T  invoke(RecursiveTask):启动根任务;

主要开发步骤:

1、定义任务,至少有两种类型的任务,按可分和不可分来辨别;

2、编写各种任务的class,主要难点在可分任务那里,要加if判断,判断要fork成哪种类型的任务,并且在fork之前将任务保存在一个list中

3、每个可分任务最后都要将子任务list给for-each出来,并且执行join得到各个子任务的结果进行处理后返回;

4、使用ForkJoinPool的invoke,将根任务放进去,然后就能返回结果了;

你可能感兴趣的:(#,并发编程,#,Java,并发编程,thread,多进程)