Java并发-并行流源码分析

以求和为例,进行并行流的源码分析

并行流求和示例

求和的方式有很多种

第一种 long sum = LongStream.rangeClosed(1, 10).parallel().sum();
第二种 long sum = LongStream.rangeClosed(1, 10).parallel().reduce(0, Long::sum);
第三种 long sum = list.parallelStream().reduce(Long::sum).get();

源码分析

以第三种为例, Collection#parallelStream

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
    return StreamSupport.stream(spliterator(), true);
}

StreamSupport#stream 并行流和顺序流的区别就在于参数 parallel, 改参数最终对应的是AbstractPipeline#parallel属性。

public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
    Objects.requireNonNull(spliterator);
    return new ReferencePipeline.Head<>(spliterator,
                                        StreamOpFlag.fromCharacteristics(spliterator),
                                        parallel);
}

再看reduce方法。

Stream#reduce --> ReferencePipeline#reduce --> AbstractPipeline#evaluate

final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
    assert getOutputShape() == terminalOp.inputShape();
    if (linkedOrConsumed)
        throw new IllegalStateException(MSG_STREAM_LINKED);
    linkedOrConsumed = true;
	// 根据parallel属性选择走并行还是顺序操作
    return isParallel()
           ? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
           : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}

下面直接看并行操作

TerminalOp#evaluateParallel 这里是默认方法实现, 默认实现还是走的是顺序。

default <P_IN> R evaluateParallel(PipelineHelper<E_IN> helper,
                                  Spliterator<P_IN> spliterator) {
    if (Tripwire.ENABLED)
        Tripwire.trip(getClass(), "{0} triggering TerminalOp.evaluateParallel serial default");
    return evaluateSequential(helper, spliterator);
}

我们这里的TerminalOp是实际是ReduceOp, 重写了方法。

// 顺序
@Override
public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
                                   Spliterator<P_IN> spliterator) {
    return helper.wrapAndCopyInto(makeSink(), spliterator).get();
}
// 并行
@Override
public <P_IN> R evaluateParallel(PipelineHelper<T> helper,
                                 Spliterator<P_IN> spliterator) {
    return new ReduceTask<>(this, helper, spliterator).invoke().get();
}

可以看到并行流走的是ReduceTask。从UML图可以看到继承了AbstractTask 从而也继承了ForkJoinTask,这也印证了并行流的底层是依赖了Fork/Join框架。

image-20240201162058789

继续跟踪invoke方法。 走的是ForkJoinTask#invoke

public final V invoke() {
    int s;
    // 执行,Done状态 1 << 31,int类型小于0 其它状态都需要awaitDone
    if ((s = doExec()) >= 0)
        s = awaitDone(null, true, false, false, 0L);
    if ((s & ABNORMAL) != 0)
        reportException(s);
    // 从AbstractTask的属性localResult中获取值
    return getRawResult();
}

final int doExec() {
    int s; boolean completed;
    if ((s = status) >= 0) {
        try {
            // 执行
            completed = exec();
        } catch (Throwable rex) {
            s = trySetException(rex);
            completed = false;
        }
        // 完成状态 setDone
        if (completed)
            s = setDone();
    }
    return s;
}

其中exec方法在CountedCompleter#exec实现了

protected final boolean exec() {
    compute();
    return false;
}

下面就是Fork/Join任务执行的核心方法compute, 在AbstractTask中进行处理。我们自己在写Fork/Join任务的时候也是要重写这个compute方法。

public void compute() {
    Spliterator<P_IN> rs = spliterator, ls; // right, left spliterators
    // 预估分片中数据量
    long sizeEstimate = rs.estimateSize();
    // 根据预估的数据量计算最小单元阈值
    long sizeThreshold = getTargetSize(sizeEstimate);
    boolean forkRight = false;
    @SuppressWarnings("unchecked") K task = (K) this;
    // 当前分片数量 > 阈值 &&  分片
    while (sizeEstimate > sizeThreshold && (ls = rs.trySplit()) != null) {
        K leftChild, rightChild, taskToFork;
        // 拆分左任务和右任务 
        task.leftChild  = leftChild = task.makeChild(ls);
        task.rightChild = rightChild = task.makeChild(rs);
        // 维护挂起子任务的pending
        task.setPendingCount(1);
        // 交替左右fork 可以优化拆分算法导致数据分片不均性能变差的情况
        if (forkRight) {
            forkRight = false;
            rs = ls;
            task = leftChild;
            taskToFork = rightChild;
        }
        else {
            forkRight = true;
            // 一半任务继续执行
            task = rightChild;
            // 一半任务加入队列
            taskToFork = leftChild;
        }
        // fork, 等待线程池处理
        taskToFork.fork(); 
        sizeEstimate = rs.estimateSize();
    }
    // 当任务已经分解到足够小的时候退出循环,尝试进行结束。
    // doLeaf方法,串行完成最小计算单元的任务,并将结果设置到当前任务的localResult中
    task.setLocalResult(task.doLeaf());
    // 合并左右任务结果等操作
    task.tryComplete();
}

其中 AbstractTask#getTargetSize 计算最小单元阈值。

private static final int LEAF_TARGET = ForkJoinPool.getCommonPoolParallelism() << 2;

public static long suggestTargetSize(long sizeEstimate) {
    long est = sizeEstimate / getLeafTarget();
    return est > 0L ? est : 1L;
}

public static int getLeafTarget() {
    Thread t = Thread.currentThread();
    if (t instanceof ForkJoinWorkerThread) {
        return ((ForkJoinWorkerThread) t).getPool().getParallelism() << 2;
    }
    else {
        // LEAF_TARGET = ForkJoinPool.getCommonPoolParallelism() << 2;
        // 机器8c, 7 * 4 = 28
        return LEAF_TARGET;
    }
}

ArrayList.ArrayListSpliterator#trySplit 通过二分法进行拆分

public ArrayListSpliterator trySplit() {
    int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
    return (lo >= mid) ? null : // divide range in half unless too small
        new ArrayListSpliterator(lo, index = mid, expectedModCount);
}

ReduceOps.ReduceTask#makeChild 方法得到的子任务中spliterator 就是上面二分的spliterator

比如第一次循环

task.leftChild 中 index 0 fence 5

task.rightChild 中 index 5 fence 10

protected ReduceTask<P_IN, P_OUT, R, S> makeChild(Spliterator<P_IN> spliterator) {
    return new ReduceTask<>(this, spliterator);
}

每次选一半的task进行fork,放入任务队列,然后另一半task接着进行拆分

  • task:下次循环接着拆分的
  • tasksToFork:进行fork放入队列的

在tasksToFork.fork方法放入队列后, 当取到该任务时,还会再次调用compute方法继续进行拆分。 这里就是Fork/Join相关的技术实现。如下图所见, 这里第一次循环的leftChild又一次走进来compute方法。

大致的调用链路如下:

  • 加入队列, 创建线程

ForkJoinPool#externalPush --> ForkJoinPool#signalWork --> ForkJoinPool#createWorker --> ForkJoinWorkerThread.start

  • 线程运行

ForkJoinWorkerThread#run --> ForkJoinWorkerThread#runWorker --> ForkJoinPool#scan --> ForkJoinPool.WorkQueue#topLevelExec --> ForkJoinTask#doExec --> CountedCompleter#exec --> AbstractTask#compute

image-20240204154510194

ReduceOps.ReduceTask#doLeaf 串行完成叶子任务的计算

@Override
protected S doLeaf() {
    return helper.wrapAndCopyInto(op.makeSink(), spliterator);
}

CountedCompleter#tryComplete 合并结果

public final void tryComplete() {
    CountedCompleter<?> a = this, s = a;
    for (int c;;) {
        // pending为0 , 任务完成
        if ((c = a.pending) == 0) {
            a.onCompletion(s);
            if ((a = (s = a).completer) == null) {
                s.quietlyComplete();
                return;
            }
        }
        // 如果挂起计数为非0,则递减计数
        else if (a.weakCompareAndSetPendingCount(c, c - 1))
            return;
    }
}

ReduceOps.ReduceTask#onCompletion

@Override
public void onCompletion(CountedCompleter<?> caller) {
    // 如果该任务是中间节点会将左右子节点的结果进行合并
    if (!isLeaf()) {
        // 左任务结果
        S leftResult = leftChild.getLocalResult();
        // 左任务结果 合并(sum方法此处为 +) 右任务结果
        leftResult.combine(rightChild.getLocalResult());
        setLocalResult(leftResult);
    }
    // GC spliterator, left and right child 
    // 左右任务都置空
    super.onCompletion(caller);
}

【本文完】

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值