JDK8新特性8——并行流与Fork/join框架

前面介绍的stream流都是串行的流,就是在一个线程上执行。接下来介绍获取并行流的两种方式

1. 直接获取并行流——parallelStream

parallelStream其实就是一个并行执行的流,它通过默认的ForkJoinPool,可以提高多线程任务的执行速度。

例如:

List<String> list = new ArrayList<>();
Stream<String> stringStream = list.parallelStream();

2. 将串行流转成并行流——parallel

我们将串行的流转成并行流可以使用方法parallel

例如:

Stream<String> parallel = list.stream().parallel();
System.out.println(parallel.isParallel());  // true

3. 并行流的使用

并行流的使用与串行流的使用方式一样

例如:

    package com.bjc.jdk8.col;

    import com.bjc.jdk8.pojo.Student;

    import java.lang.reflect.Array;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    public class StreamCollectTest02 {
        public static void main(String[] args) {
            List<Integer> list = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 11, 22, 3, 3, 33, 4, 4, 44, 55, 444)
                    .parallel()
                    .filter(num -> {
                        System.out.println("当前线程:" + Thread.currentThread().getName());
                        return num > 3;
                    })
                    .collect(Collectors.toList());
            System.out.println(list);
        }
    }

输出结果:

可以发现,并行流是多线程操作的,所以执行效率要高于串行流。

4. 解决并行流parallelStream的安全问题

        Stream并行流处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作,这样当我们操作并行流的时候,就可能会出现线程安全问题了。

例如:

public class StreamCollectTest03 {
	public static void main(String[] args) {
	    List list = new ArrayList();
	    LongStream.rangeClosed(1, 10000).parallel().forEach(s->list.add(s));
	    System.out.println(list.size());
	}
}

理论上,输出结果应该是10000,但是实际输出结果为;

注意:这个输出结果每次输出可能都不同,而且也有可能报数组越界的错误。错误信息如下:

那么怎么解决这个问题了?

4.1 使用同步代码块解决

例如:

我们可以将list.add放在synchronized同步语句块中,运行结果

4.2 使用线程安全集合

例如:

public class StreamCollectTest03 {
	public static void main(String[] args) {
	    ConcurrentLinkedDeque<Long> cld = new ConcurrentLinkedDeque();
	    // List list = new ArrayList();
	    LongStream.rangeClosed(1, 10000).parallel().forEach(s->{
		cld.add(s);
	    });
	    System.out.println(cld.size());
	}
}

4.3 调用stream流的collector或者toArray方法

例如:

public class StreamCollectTest03 {
	public static void main(String[] args) {
	    List<Long> list = LongStream.rangeClosed(1, 10000).parallel().boxed().collect(Collectors.toList());
	    System.out.println(list.size());
	}
}

5. Fork/Join框架

parallelStream使用的是Fork/Join框架,该框架自1.7开始引入,可以将一个大任务拆分为很多小任务来异步执行。

Fork/Join主要包含三部分:

1)线程池:ForkJoinPool

2)任务对象:ForkJoinTask

3)执行任务的线程:ForkJoinWorkerThread

5.1 原理

5.1.1 分治法

        ForkJoinPool主要用来使用分治法解决问题。典型的应用比如快速排序算法,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序和一个针对这两组数组500万数据的合并任务。以此类推,针对500万数据也会做出同样的分割处理,到最后,会设置一个阈值来规定当数据规模到多大的时候,停止这样的分割处理。

        比如当元素的数量小于10的时候,会停止分割,转而使用插入排序对他们进行排序,那么到最后,所有的任务加起来会有很多个,问题的关键在于,对于一个任务而言,只有当他的所有子任务完成之后,它才能够被执行。

如图:

5.1.2 工作窃取算法

          Fork/Join最核心的地方就是利用了现代硬件设备多核,在一个操作时候会有空闲的cpu,那么如何利用好这个空闲的cpu就成了提高性能的关键,而这里我们要提到的工作窃取(work-stealing)算法就是整个Fork/Join框架的核心理念, Fork/Join工作窃取算法指的是某个线程从其他队列里窃取任务来执行。

        那么为什么要使用工作窃取算法了?假如我们有一个很大的任务,我们可以把这些任务分割成若干个互不依赖的子任务,为了减少线程间的竞争,于是把这些线程分割放在不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应。

        比如线程A负责处理A队列的任务,但是有的线程总会先把自己队列的任务处理完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行,而这时候,两个线程会访问同一个队列,所以为了减少窃取任务线程与被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。如上图所示,线程2提前执行完了其队列中的任务,然后去线程1的队列的尾部窃取了任务来执行。

        工作窃取算法的有点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点也是存在的

1)在某些情况下还是会存在竞争,比如,双端队列只有一个任务的时候。

2)会消耗更多的系统资源,比如创建多个线程和多个双端队列

我们使用并行流parallelStream它能够让一部分java代码自动的以并行的方式执行,也就是使用了ForkJoinPool的ParallelStream

        对于ForkJoinPool通过线程池的线程数量,通常下使用默认值就可以了,即运行时计算机的处理器数量,可以通过设置系统属性:java.util.concurrent.ForkJoinPool.common.parallelism=N(N为线程数量)来调整ForkJoinPool的线程数量。

5.2 Fork/Join的使用

        一般情况下,我们需要定义一个任务类,继承RecursiveTask,并实现方法compute。

使用分为3步:

1)定义任务类ForkJoinTask继承RecursiveTask

2)创建ForkJoinPool连接池

3)执行任务

接下来我们通过一个案例,来看看Fork/Join的简单使用。

5.3 Fork/Join案例

需求:使用Fork/Join计算1-10000的和,当一个任务的计算数量大于3000的时候拆分任务,数量小于3000时计算

1)定义任务类ForkJoinTask继承RecursiveTask

package com.bjc.jdk8.forkjoin;

import java.util.concurrent.RecursiveTask;

/**
 * 任务类
 */
public class ForkJoinTask extends RecursiveTask<Long> {

    // 是否要拆分的临界值
    private final static Long THREAFHOLD = 3000L;
    // 起始值
    private final Long start;
    // 结束值
    private final Long end;

    public ForkJoinTask(Long start,Long end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        // 定义当前数据的长度
        long length = end-start;
        // 计算
        if(length <= THREAFHOLD){  // 小于拆分的临界值,不用拆分,可以计算
            Long sum = 0L;
            for(Long i=start;i<=end;i++){
                sum+=i;
            }
            return sum;
        } else {  // 继续拆分
            // 获取最大值
            Long midle = (start + end)/2;
            ForkJoinTask left = new ForkJoinTask(start,midle);
            left.fork();  // 任务拆分
            ForkJoinTask right = new ForkJoinTask(midle+1,end);
            right.fork(); // 任务拆分
            return left.join() + right.join(); // 任务合并并返回
        }
    }


}

2)创建ForkJoinPool连接池并执行

package com.bjc.jdk8.forkjoin;

import java.util.concurrent.ForkJoinPool;

public class ForkJoinDemo {
    public static void main(String[] args) {
        long start = System.nanoTime();
        // 定义任务
        ForkJoinTask task = new ForkJoinTask(0L,99999999999L);
        // 创建ForkJoinPool连接池
        ForkJoinPool pool = new ForkJoinPool();
        // 执行任务
        Long invoke = pool.invoke(task);
        long end = System.nanoTime();
        System.out.println(invoke);
        System.out.println(end-start);
    }
}

小结:

1)parallelStream是线程不安全的

2)parallelStream适用的场景是CPU密集型的,只是做到不浪费CPU,假如本身电脑CPU的负载很大,那还到处使用并行流,那并不能起到作用

3)I/O密集型、磁盘I/O、网络I/O都属于I/O操作,这部分操作是较少的消耗CPU资源,一般并行流中并不适用于I/O密集型的操作,就比如用并行流进行大批量的消息推送,涉及到了大量的I/O,使用并行流反而会慢很多

4)在使用并行流的时候是无法保证元素顺序的,也就是即使你使用了同步集合也只能保证数据的正确但无法保证元素的顺序。

发布了205 篇原创文章 · 获赞 9 · 访问量 7916

猜你喜欢

转载自blog.csdn.net/weixin_43318134/article/details/104444633