Java8 并行流原理

一、并行流的简单使用

    public static void main(String[] args) throws InterruptedException {
    
    
    	//设置睡眠时间,方便visualVM监控到当前应用
        Thread.sleep(25000);

        //groupingBy分组 键就是组名【返回值做组名】,Map的值就是该组的集合【满足条件的值】
        Arrays.asList(1, 2, 3, 4, 5, 6, 7, 9, 8, 0, 1,1,2,3,4,5,9)
                .stream()
                .parallel()
                .collect(Collectors.groupingBy(x -> {
    
    
                	//控制台查看当前线程
                    System.out.println(Thread.currentThread().getName());
                    try {
    
    
                    	//执行慢点,方便visualVM上查看线程
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    int i = x % 10;
                    return i;
                }))
                .forEach((x, y) -> System.out.println(x + ":" + y));
    }

visualVM查看到的线程:发现并行流使用的是ForkJoin的线程池,猜测底层使用的是ForkJoin
在这里插入图片描述
控制台:可以看到main线程也是参与了计算的过程

main
ForkJoinPool.commonPool-worker-5
ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-3
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-4
ForkJoinPool.commonPool-worker-1
ForkJoinPool.commonPool-worker-6
ForkJoinPool.commonPool-worker-7
ForkJoinPool.commonPool-worker-3
main
ForkJoinPool.commonPool-worker-5
0:[0]
1:[1, 1, 1]
2:[2, 2]
3:[3, 3]
4:[4, 4]
5:[5, 5]
6:[6]
7:[7]
8:[8]
9:[9, 9]

ForkJoinPool如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。

1、我的CPU为8核,为啥只有七条线程?

由于 taskToFork.fork() 调用【这个后面源码解析,会说】,parallize使用了默认的ForkJoinPool.common 默认的一个静态线程池,这个线程池的默认线程个数是cpu 数量-1
JDK8以后,ForkJoinPool又提供了一个静态方法commonPool(),这个方法返回一个ForkJoinPool内部声明的静态ForkJoinPool实例。

//使用commonPool创建一个ForkJoin线程池
ForkJoinPool forkJoinPool = ForkJoinPool.commonPool();
ForkJoinPool类:
public static ForkJoinPool commonPool() {
    
    
     // assert common != null : "static init error";
     return common;
 }
static {
    
    
xxx省略
    common = java.security.AccessController.doPrivileged
        (new java.security.PrivilegedAction<ForkJoinPool>() {
    
    
            public ForkJoinPool run() {
    
     return makeCommonPool(); }});
    int par = common.config & SMASK; // report 1 even if threads disabled
    commonParallelism = par > 0 ? par : 1;
}
//可以看到线程数为Runtime.getRuntime().availableProcessors() - 1
//即cpu核数-1
private static ForkJoinPool makeCommonPool() {
    
    
xxx省略
    if (parallelism < 0 && // default 1 less than #cores
        (parallelism = Runtime.getRuntime().availableProcessors() - 1) <= 0)
        parallelism = 1;
    if (parallelism > MAX_CAP)
        parallelism = MAX_CAP;
    return new ForkJoinPool(parallelism, factory, handler, LIFO_QUEUE,
                            "ForkJoinPool.commonPool-worker-");
}

2、如何控制parallize的线程数?

1、启动参数指定-Djava.util.concurrent.ForkJoinPool.common.parallelism=3
2、代码指定System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","3");
3、可以自己构建一个ForkJoinPool,向其中提交一个parallize并行任务,可以做到控制并发度。例如:

    public static void main(String[] args) throws InterruptedException, ExecutionException {
    
    
       ForkJoinPool pool = new ForkJoinPool(2);
       pool.execute(()->{
    
    
           //groupingBy分组 键就是组名【返回值做组名】,Map的值就是该组的集合【满足条件的值】
           Arrays.asList(1, 2, 3, 4, 5, 6, 7, 9, 8, 0, 1,1,2,3,4,5,9)
                   .stream()
                   .parallel()
                   .collect(Collectors.groupingBy(x -> {
    
    
                       System.out.println(Thread.currentThread().getName());

                       int i = x % 10;
                       return i;
                   }))
                   .forEach((x, y) -> System.out.println(x + ":" + y));
       });
        Thread.sleep(2000);
    }

注:一般不建议修改,因为修改虽然改进当前的业务逻辑,但对于整个项目中其它地方只是用来做非耗时的并行流运算,性能就不友好了,因为所有使用并行流parallerStream的地方都是使用同一个Fork-Join线程池,而Fork-Join线程数默认仅为cpu的核心数。最好是自己创建一个Fork-Join线程池来用,即方法3。
对于CPU密集型的任务,cpu的核数作为最大线程数,可以保持cpu的效率最高。因此,一般是不建议修改,修改也是使用第三种方式针对某个业务进行特定的修改就行,不要全局修改。

二、源码解析

我们知道Stream是一个惰性求值的系统,那么我们只需要找它最后求值的过程,看它是怎样进行求值的就可以了。在AbstractPipeline这个类里面我们找到了Stream 计算的最终求值过程的默认实现:

final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
    
    
    assert getOutputShape() == terminalOp.inputShape();
    if (linkedOrConsumed)
        throw new IllegalStateException(MSG_STREAM_LINKED);
    linkedOrConsumed = true;

    return isParallel()
8? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
           : terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}

在8行,我们可以看到,在求值的时候会检查并行计算的标志位,如果标志了并行计算的话,我们就会并行求值,反之则会串行求值。我们可以进一步进入并行求值的逻辑中,这是一个TerminalOp的默认接口方法,默认实现就是直接调用串行求值,在FindOp、ForEachOp、MatchOp 和 ReduceOp中得到了覆盖。
在这里插入图片描述
随便进一个:比如FindOp

@Override
public <P_IN> O evaluateParallel(PipelineHelper<T> helper,
                                 Spliterator<P_IN> spliterator) {
    
    
    return new FindTask<>(this, helper, spliterator).invoke();
}

发现FindOp底层是直接new了一个TaskForEachOp、MatchOp、ReduceOp也都是创建一个Task,然后执行invoke方法。

看看这个Task是个什么玩意儿
在这里插入图片描述

可以看出所有的Task 都继承自Jdk7 中引入的ForkJoin 并行框架的ForkJoinTask。所以我们可以看出Stream 的并行是依赖于ForkJoin 框架的。

ForkJoin进行计算任务时,计算类是要继承ForkJoinTask并且重写compute方法的。
AbstractTask为例我们看看它是如何进行并行计算的:

@Override
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);
        task.setPendingCount(1);
        if (forkRight) {
    
    
            forkRight = false;
            rs = ls;
            task = leftChild;
            taskToFork = rightChild;
        }
        else {
    
    
            forkRight = true;
            task = rightChild;
            taskToFork = leftChild;
        }
        taskToFork.fork();
        sizeEstimate = rs.estimateSize();
    }
    task.setLocalResult(task.doLeaf());
    task.tryComplete();
}
 
ReduceTask(ReduceTask<P_IN, P_OUT, R, S> parent,
           Spliterator spliterator) {
    
    
    super(parent, spliterator);
    this.op = parent.op;
}
 
@Override
protected ReduceTask<P_IN, P_OUT, R, S> makeChild(Spliterator spliterator) {
    
    
    return new ReduceTask<>(this, spliterator);
}

这里面的主要逻辑就是:

  • 先调用当前splititerator 方法的estimateSize 方法,预估这个分片中的数据量
  • 根据预估的数据量获取最小处理单元的大小阈值,即当数据量已经小于这个阈值的时候进行计算,否则进行fork
    将任务划分成更小的数据块,进行求解。这里值得注意的是,getTargetSize 在第一次调用的时候会设置:
  • 预测数据量大小 / (默认并发度 * 4) 的结果作为最小执行单元的数量(配置的默认值是cpu 数 –
    1,可以通过java.util.concurrent.ForkJoinPool.common.parallelism设置)
  • 如果当前分片大小仍然大于处理数据单元的阈值,且分片继续尝试切分成功,那么就继续切分,分别将左右分片的任务创建为新的Task,并且将当前的任务关联为两个新任务的父级任务(逻辑在makeChild
    里面)
  • 先后对左右子节点的任务进行fork,对另外的分区进行分解。同时设定pending 为1,这代表一个task
    实际上只会有一个等待的子节点(被fork)。
  • 当任务已经分解到足够小的时候退出循环,尝试进行结束。调用子类实现的doLeaf方法,完成最小计算单元的计算任务,并设置到当前任务的localResult中
  • 调用tryComplete 方法进行最终任务的扫尾工作,如果该任务pending
    值不等于0,则原子的减1,如果已经等于0,说明任务都已经完成,则调用onCompletion
    回调,如果该任务是叶子任务,则直接销毁中间数据结束;如果是中间节点会将左右子节点的结果进行合并
  • 检查如果这个任务已经没有父级任务了,则将该任务置为正常结束,如果还有则尝试递归的去调用父级节点的onCompletion回调,逐级进行任务的合并。

回到问题我的CPU为8核,为啥只有七条线程?
taskToFork.fork()的调用就是在compute这里被调用,common就是一个静态的线程池
在这里插入图片描述

好了,基本就这样。

猜你喜欢

转载自blog.csdn.net/weixin_42412601/article/details/108982398