Stream总结

目录

流是什么

生成流

使用方法

map/flatMap

filter

forEach

peek

聚合操作reduce

收集结果collect

将结果收集到map中

分组和分片

join和统计功能

并发流及其效率

并发的演变

生成并发流

并发性能测试

collect()保证有序性

并发流的实现方法

parallelStream的实现:ForkJoinPool

什么时候用并发流?

 

流是什么

Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。它也不同于 StAX 对 XML 解析的 Stream,也不是 Amazon Kinesis 对大数据实时处理的 Stream。Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

上面是官方的解释,我认为流是一种处理数据的思路,或者说是一种更好的方法,以前我们总是把数据一个一个遍历元素并对其执行某些操作, 使用流就是我先定义要进行什么操作,Stream 会隐式地在内部进行遍历,做出相应的数据转换。

那不是还要进行遍历吗?没有节省开支啊. 

首先我们节省了代码量,使用了更优雅的代码; 其次对于空间复杂度,如果入参是流的形式,就可以大大的节省内存占用,意思就是我们之前的数据是全部放入内存中再进行处理,非常巨大的集合类会占用大量的内存,而现在可以来一个数据就用我定义好的处理方式处理一个,因为Stream的元素是在访问的时候才被计算出来(延迟计算),而不用占用大量的内存.而且对于时间复杂度,Stream 依赖于 Java7 中引入的 Fork/Join 框架可以实现并行, 来拆分任务和加速处理过程。

  1. 对于大量数据可以不存入内存, 流式的处理
  2. 更简洁的代码
  3. 可以很方便的并行处理

总结一下,对集合进行代码简洁,清晰但不一定易读(可读性其实不高),不一定高效(效率其实有时不如for循环) 的转换、过滤、聚合等操作,“高级版”的迭代器,而和迭代器又不同的是,Stream可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出(但并发功能慎用,不方便debug,不抛出checked异常

stream的意义是更优雅(简洁)的实现一些数据操作(主要是迭代),但效率不一定比传统写法高.因为其简洁性,不应该在其中写入太多代码.

生成流

当我们使用一个流的时候,通常包括三个基本步骤:

获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道.

流的操作类型分为两种:

  • Intermediate(中间操作):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
  • Terminal(终端操作):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

流是一个懒加载的模式 未结束流前不会进行操作,只要结束流以后才会操作返回结果.

                                                                                            Stream操作分类

中间操作(Intermediate operations)

无状态(Stateless)

map() mapToInt() mapToLong() mapToDouble()

flatMap() flatMapToInt() flatMapToLong() flatMapToDouble()

filter()

peek()

unordered()

有状态(Stateful)

distinct() sorted() limit() skip()

终端操作(Terminal operations)

非短路操作

forEach() forEachOrdered() toArray() reduce() collect() max() min() count()

短路操作(short-circuiting)

anyMatch() allMatch() noneMatch() findFirst() findAny()

1. 直接对collection.stream()

List<String> list = Arrays.asList("1","2","3","4");
Stream<String> stream1 = list.stream();

2.对于数组

String[] s = new String[]{"1","2","3","4"};
Stream<String> stream2 = Arrays.stream(s);

3.直接构建一个流

Stream<String> stream3 = Stream.of("1","2","3","4");

4.读取文件流

try (Stream<String> stream4 =
         Files.lines(Paths.get("D:\\data.txt"), Charset.defaultCharset())) {
    stream4.forEach(System.out::print);
} catch (IOException e) {
    //try()是一个语法糖,用来自动关闭括号中的资源
}

5.创建无限流

//0开始 每次+1 创建
Stream<Integer> stream5 = Stream.iterate(0,(x)‐>x+1);

//随机数
Stream<Double> stream6 = Stream.generate(Math::random);

可能会有疑问,流不会无限创建下去吗? 流是延迟计算的,这里只是定义,在使用流的时候进行规范就可以了

//对刚刚定义的流进行输出 限制为10位
stream6.limit(10).forEach(System.out::print);

stream() 获取当前小物件的 source,filter 和 mapToInt 为 intermediate 操作,进行数据筛选和转换,最后一个 sum() 为 terminal 操作,对符合条件的全部小物件作重量求和。

流的简单使用就是对一串输入数据的处理,本来非常复杂的处理可以使用简洁的代码实现

使用方法

前面说过流就是定义一些想要执行的操作, 以及结束操作. 那怎么定义呢, 简单来说就是方法连点. 

1.流的构造与转换

2.流的操作

3.终端操作

这里主要总结一些常用操作, 其他方法可以到这里查看.

方法字典:

sort 排序 [倒序 .reversed()]

filter 过滤 

distinct 去重

limit(n) 流取前n个

skip(n) 忽略前n个

forEach 遍历

peek 对流进行任何操作,但不影响流

map/flatMap

我们先来看 map。它的作用就是把 input Stream 的每一个元素,映射成 output Stream 的另外一个元素。

简单来说就是 输入集合返回新集合

List<HistoryList> historyLists =  diffs.stream().map(
                diff -> new HistoryList()
                        .setId(diff.getId())
                        .setDiff(diff.getDiffMsg())
                        .setCompareTime(diff.getDiffTime())
                        .setSourceData(String.valueOf(ReadUtils.readFile(diff.getFile1Url())))
                        .setTargetData(String.valueOf(ReadUtils.readFile(diff.getFile2Url())))
        ).collect(Collectors.toList());

输入集合diffs 返回新集合List<HistoryList>

filter

filter 对原始 Stream 进行过滤(其实就是if为true),被留下来的元素生成一个新 Stream。

 留下偶数

Integer[] sixNums = {1, 2, 3, 4, 5, 6};
Integer[] evens =
Stream.of(sixNums).filter(n -> n%2 == 0).toArray(Integer[]::new);

把单词挑出来经过条件“被 2 整除”的 filter,剩下的数字为 {2, 4, 6}。

List<String> output = reader.lines().
 flatMap(line -> Stream.of(line.split(REGEXP))).
 filter(word -> word.length() > 0).
 collect(Collectors.toList());

forEach

可以对一个stream的每个元素进行一个处理 没有返回值 既接收一个 Lambda 表达式,然后在 Stream 的每一个元素上执行该表达式。

打印姓名

// stream
roster.stream()
 .filter(p -> p.getGender() == Person.Sex.MALE)
 .forEach(p -> System.out.println(p.getName()));

// foreach
for (Person p : roster) {
 if (p.getGender() == Person.Sex.MALE) {
 System.out.println(p.getName());
 }
}

但一般认为,forEach 和常规 for 循环的差异不涉及到性能,它们仅仅是函数式风格与传统 Java 风格的差别。对一个人员集合遍历,找出男性并打印姓名。可以看出来,forEach 是为 Lambda 而设计的,保持了最紧凑的风格。而且 Lambda 表达式本身是可以重用的,非常方便。当需要为多核系统优化时,可以 parallelStream().forEach(),只是此时原有元素的次序没法保证,并行的情况下将改变串行时操作的行为,此时 forEach 本身的实现不需要调整,而 Java8 以前的 for 循环 code 可能需要加入额外的多线程逻辑。

另外一点需要注意,forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次 terminal 运算。下面的代码是错误的:

stream.forEach(element -> doOneThing(element));
stream.forEach(element -> doAnotherThing(element));

peek

对每个元素执行操作并返回一个新的 Stream相反,就是这里可以随意对流做操作, 但返回的结果还是原来的流.

Stream.of("one", "two", "three", "four")
 .filter(e -> e.length() > 3)
 .peek(e -> System.out.println("Filtered value: " + e))
 .map(String::toUpperCase)
 .peek(e -> System.out.println("Mapped value: " + e))
 .collect(Collectors.toList());

 

聚合操作reduce

    Stream.reduce,返回单个的结果值,并且reduce操作每处理一个元素总是创建一个新值。常用的方法有average, sum, min, max, count,使用reduce方法都可实现。这里主要介绍reduce方法:

T reduce(T identity, BinaryOperator<T> accumulator)

   identity:它允许用户提供一个循环计算的初始值。accumulator:计算的累加器,其方法签名为apply(T t,U u),在该reduce方法中第一个参数t为上次函数计算的返回值,第二个参数u为Stream中的元素,这个函数把这两个值计算apply,得到的和会被赋值给下次执行这个方法的第一个参数。有点绕看代码:

int value = Stream.of(1, 2, 3, 4).reduce(100, (sum, item) -> sum + item);
Assert.assertSame(value, 110);
/* 或者使用方法引用 */
value = Stream.of(1, 2, 3, 4).reduce(100, Integer::sum);

  这个例子中100即为计算初始值,每次相加计算值都会传递到下一次计算的第一个参数。

reduce还有其它两个重载方法:

  • Optional<T> reduce(BinaryOperator<T> accumulator):与上面定义基本一样,无计算初始值,所以他返回的是一个Optional。
  • <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner):与前面两个参数的reduce方法几乎一致,你只要注意到BinaryOperator其实实现了BiFunction和BinaryOperator两个接口。

 

收集结果collect

方法字典:

collect(counting()) 计数

collect(summingInt()/summingDouble()/summingLong()) 求和

collect(avgeragingInt()) 平均

collect(maxBy()/minBy()) 最值 

collect(groupingBy()) 分组

  当你处理完流时,通常只是想查看一下结果,而不是将他们聚合为一个值。先看collect的基础方法,它接受三个参数:

<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)

supplier:一个能创造目标类型实例的方法。accumulator:一个将当元素添加到目标中的方法。combiner:一个将中间状态的多个结果整合到一起的方法(并发的时候会用到)。接着看代码:

Stream<Integer> stream = Stream.of(1, 2, 3, 4).filter(p -> p > 2);
List<Integer> result = stream.collect(() -> new ArrayList<>(), (list, item) -> list.add(item), (one, two) -> one.addAll(two));
/* 或者使用方法引用 */
result = stream.collect(ArrayList::new, List::add, List::addAll);

这个例子即为过滤大于2的元素,将剩余结果收集到一个新的list中。

  • 第一个方法生成一个新的ArrayList;
  • 第二个方法中第一个参数是前面生成的ArrayList对象,第二个参数是stream中包含的元素,方法体就是把stream中的元素加入ArrayList对象中。第二个方法被反复调用直到原stream的元素被消费完毕;
  • 第三个方法也是接受两个参数,这两个都是ArrayList类型的,方法体就是把第二个ArrayList全部加入到第一个中;

代码有点繁琐,或者使用collect的另一个重载方法:

<R,A> R collect(Collector<? super T,A,R> collector)

注意到Collector其实是上面supplier、accumulator、combiner的聚合体。那么上面代码就变成:

List<Integer> list = Stream.of(1, 2, 3, 4).filter(p -> p > 2).collect(Collectors.toList());

将结果收集到map中

先定义如下Person对象

class Person{
    public String name;
    public int age;

    Person(String name, int age){
      this.name = name;
      this.age = age;
    }

    @Override
    public String toString(){
      return String.format("Person{name='%s', age=%d}", name, age);
    }
  }

假设你有一个Stream<Person>对象,希望将其中元素收集到一个map中,这样就可以根据他的名称来查找对应年龄,例如:

Map<String, Integer> result = people.collect(HashMap::new,(map,p)->map.put(p.name,p.age),Map::putAll);
/*使用Collectors.toMap形式*/
Map<String, Integer> result = people.collect(Collectors.toMap(p -> p.name, p -> p.age, (exsit, newv) -> newv));

其中Collectors.toMap方法的第三个参数为键值重复处理策略,如果不传入第三个参数,当有相同的键时,会抛出一个IlleageStateException。

或者你想将Person分解为Map存储:

List<Map<String, Object>> personToMap = people.collect(ArrayList::new, (list, p) -> {
   Map<String, Object> map = new HashMap<>();
   map.put("name", p.name);
   map.put("age", p.age);
   list.add(map);
}, List::addAll);

分组和分片

对具有相同特性的值进行分组是一个很常见的任务,Collectors提供了一个groupingBy方法,方法为:

<T,K,A,D> Collector<T,?,Map<K,D>> groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream)

classifier:一个获取Stream元素中主键方法。downstream:一个操作对应分组后的结果的方法。

假如要根据年龄来分组:

Map<Integer, List<Person>> peropleByAge = people
    .filter(p -> p.age > 12)
    .collect(Collectors.groupingBy(p -> p.age, Collectors.toList()));

假如我想要根据年龄分组,年龄对应的键值List存储的为Person的姓名,怎么做呢:

Map<Integer, List<String>> peropleByAge = people
    .collect(Collectors.groupingBy(
        p -> p.age, Collectors.mapping((Person p) -> p.name, Collectors.toList())));

mapping即为对各组进行投影操作,和Stream的map方法基本一致。

假如要根据姓名分组,获取每个姓名下人的年龄总和(好像需求有些坑爹):

Map<String, Integer> sumAgeByName = people.collect(
    Collectors.groupingBy(
    p -> p.name, Collectors.reducing(0, (Person p) -> p.age, Integer::sum)));
/* 或者使用summingInt方法 */
sumAgeByName = people.collect(
    Collectors.groupingBy(
    p -> p.name, Collectors.summingInt((Person p) -> p.age)));

可以看到Java8的分组功能相当强大,当然你还可以完成更复杂的功能。另外Collectors中还存在一个类似groupingBy的方法:partitioningBy,它们的区别是partitioningBy为键值为Boolean类型的groupingBy,这种情况下它比groupingBy更有效率。

 

join和统计功能

话说Java8中新增了一个StringJoiner,Collectors的join功能和它基本一样。用于将流中字符串拼接并收集起来,使用很简单:

String names = people.map(p->p.name).collect(Collectors.joining(","))

Collectors分别提供了求平均值averaging、总数couting、最小值minBy、最大值maxBy、求和suming等操作。但是假如你希望将流中结果聚合为一个总和、平均值、最大值、最小值,那么Collectors.summarizing(Int/Long/Double)就是为你准备的,它可以一次行获取前面的所有结果,其返回值为(Int/Long/Double)SummaryStatistics。

DoubleSummaryStatistics dss = people.collect(Collectors.summarizingDouble((Person p)->p.age));
double average=dss.getAverage();
double max=dss.getMax();
double min=dss.getMin();
double sum=dss.getSum();
double count=dss.getCount();

 

并发流及其效率

0 并发的演变

Java 的并行 API 演变历程基本如下:

  • 1.0­1.4 中的 java.lang.Thread
  • 5.0 中的 java.util.concurrent
  • 6.0 中的 Phasers 等
  • 7.0 中的 Fork/Join 框架
  • 8.0 中的 Lambda

JDK一直在完善并发相关的api, 从最开始的手动启动一个线程完成操作到增加了线程安全相关包,而在7.0中加入了自动生成线程数量合适的线程池的方法.当然我个人认为需要慎用.

1 生成并发流

直接使用如下方法就可以生成并发流:

Collection.parallelStream();

Stream.parallel()

但并发流并不是线程安全的

        List<Integer> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();
        List<Integer> list3 = new ArrayList<>();
        List<Integer> listx = new ArrayList<>();
        Lock lock = new ReentrantLock();

        long startTime=System.currentTimeMillis();   //获取开始时间
        IntStream.range(0, 10000).forEach(list1::add);
        long endTime=System.currentTimeMillis(); //获取结束时间
        System.out.println("串行执行程序运行时间: "+(endTime-startTime)+"ms");

        startTime=System.currentTimeMillis();   //获取开始时间
        IntStream.range(0, 10000).parallel().forEach(list2::add);
        endTime=System.currentTimeMillis(); //获取结束时间
        System.out.println("并行执行程序运行时间: "+(endTime-startTime)+"ms");

        startTime=System.currentTimeMillis();   //获取开始时间
        IntStream.range(0, 10000).parallel().forEachOrdered(listx::add);
        endTime=System.currentTimeMillis(); //获取结束时间
        System.out.println("并行执行forEachOrdered方法程序运行时间: "+(endTime-startTime)+"ms");

        startTime=System.currentTimeMillis();   //获取开始时间
        IntStream.range(0, 10000).parallel().forEach(i -> {
            lock.lock();
            try {
                list3.add(i);
            } finally {
                lock.unlock();
            }
        });
        endTime=System.currentTimeMillis(); //获取结束时间
        System.out.println("加锁并行执行程序运行时间: "+(endTime-startTime)+"ms");

        System.out.println("串行执行的大小:" + list1.size());
        System.out.println("并行执行的大小:" + list2.size());
        System.out.println("并行执行forEachOrdered:" + listx.size());
        System.out.println("加锁并行执行的大小:" + list3.size());

执行结果是: 

//计时实际都是分开测试的 可能因为缓存的原因 如果一起运行之后运行的代码块比第一次运行的要块10倍左右
串行执行程序运行时间: 65ms
并行执行程序运行时间: 58ms
并行执行forEachOrdered方法程序运行时间: 76ms
加锁并行执行程序运行时间: 76ms

串行执行的大小:10000
并行执行的大小:5709
并行执行forEachOrdered:10000
加锁并行执行的大小:10000

并行每次的结果中并行执行的大小不一致,而串行和加锁后的结果一直都是正确结果。显而易见,stream.parallel.forEach()中执行的操作并非线程安全  如果需要保证原来的顺序可以使用forEachOrdered()方法 或者直接加锁

从速度来看 不加限制的并行会稍微快一点 但加锁或使用forEachOrdered反而比串行还慢(以上获得的结果不是一起跑出来的,因为jvm优化的原因, 类似的操作第一次跑永远最慢)

2 并发性能测试

如果感觉流非常棒的话再看下面的例子:

long startTime = System.currentTimeMillis();   //获取开始时间
for (int i = 0; i < 100000; i++) {
    lists.add(i);
}
long endTime = System.currentTimeMillis(); //获取结束时间
System.out.println("for循环执行程序运行时间: " + (endTime - startTime) + "ms");

这段代码运行时间仅仅需要 8ms左右 是不是看到这里感觉流的性能真的很差

然而真的是这样吗? 我们尝试把数量级扩大

List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
List<Integer> lists = new ArrayList<>();

for (int i = 0; i < 10000000; i++) {
    lists.add(i);
}

long startTime1 = System.currentTimeMillis();   //获取开始时间
for (Integer list : lists) {
    list1.add(list);
}
long endTime1 = System.currentTimeMillis(); //获取结束时间
System.out.println("for执行程序运行时间: " + (endTime1 - startTime1) + "ms");


long startTimex = System.currentTimeMillis();   //获取开始时间
lists.stream().parallel().forEach(list2::add);
long endTimex = System.currentTimeMillis(); //获取结束时间
System.out.println("并行执行程序运行时间: " + (endTimex - startTimex) + "ms");

这里得到的结果是

for执行程序运行时间: 219ms
并行执行程序运行时间: 283ms

虽然还是使用for循环快一些 但是没有了在低数量级的很多倍的差距了 而这样的代价我们还是可以接受的

这里有一个大数量级的测试可以去看一下

perf_Stream_min_String

对于基本数据展示的是for循环外部迭代耗时为基准的时间比值。分析如下:

  1. 对于基本类型Stream串行迭代的性能开销明显高于外部迭代开销(两倍);
  2. Stream并行迭代的性能比串行迭代和外部迭代都好。

而如果换成负责操作

perf_Stream_reduction

Stream API的性能普遍好于外部手动迭代,并行Stream效果更佳;

 

3 collect()保证有序性

如果不想用Lock锁 或者不用forEach的forEachOrdered方法 可以使用collect() 方法在聚合数据的时候进行约束.

例如以下代码就可以保证结果与原始列表中的顺序相同

// ["a", "b", "c"]
List<String> toProcess;

// should be ["a", "b", "c"]
List<String> results = toProcess.parallelStream()
                                .map(s -> s)
                                .collect(Collectors.toList());

Stream.collect()API文档  Stream.collect()的描述如下:

If the stream is parallel, and the Collector is 07001, and either the stream is unordered or the collector is 07002, then a concurrent reduction will be performed (see 07003 for details on concurrent reduction.)

满足第一条件:流是并行的。第二和第三:是收集器并发和无序的

Collectors.toList()API文档 toList()的文档如下:

Returns a Collector that accumulates the input elements into a new List. There are no guarantees on the type, mutability, serializability, or thread-safety of the List returned; if more control over the returned List is required, use toCollection(Supplier).

Returns:
a Collector which collects all the input elements into a List, in encounter order

encounter order中工作的操作以元素的原始顺序操作元素。但文档也说了是不保证有序性的.

实现代码

检查Collectors.java的实现,确认toList()不包括CONCURRENT或UNORDERED traits。

public static <T>
Collector<T, ?, List<T>> toList() {
    return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                               (left, right) -> { left.addAll(right); return left; },
                               CH_ID);
}

// ...

static final Set<Collector.Characteristics> CH_ID
        = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));

注意收集器如何具有CH_ID trait集合,它只有单个IDENTITY_FINISH trait。 CONCURRENT和UNORDERED不存在,因此减少不能并发。

非并发减少意味着,如果流是并行的,则可以并行地进行收集,但是它将被分割成几个线程限制的中间结果,然后被组合。这确保组合结果是顺序的。

 

4 并发流的实现方法

fork/join模式自动对数据拆分,充分利用多核

fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。

3.parallelStream的实现:ForkJoinPool

要想深入的研究parallelStream之前,那么我们必须先了解ForkJoin框架和ForkJoinPool.

ForkJoin框架是从jdk7中新特性,它同ThreadPoolExecutor一样,也实现了Executor和ExecutorService接口。它使用了一个无限队列来保存需要执行的任务,而线程的数量则是通过构造函数传入,如果没有向构造函数中传入希望的线程数量,那么当前计算机可用的CPU数量会被设置为线程数量作为默认值。

ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法。这里的要点在于,ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序,那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推,对于500万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于10时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概2000000+个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行。

所以当使用ThreadPoolExecutor时,使用分治法会存在问题,因为ThreadPoolExecutor中的线程无法像任务队列中再添加一个任务并且在等待该任务完成之后再继续执行。而使用ForkJoinPool时,就能够让其中的线程创建新的任务,并挂起当前的任务,此时线程就能够从队列中选择子任务执行。

那么使用ThreadPoolExecutor或者ForkJoinPool,会有什么性能的差异呢? 
首先,使用ForkJoinPool能够使用数量有限的线程来完成非常多的具有父子关系的任务,比如使用4个线程来完成超过200万个任务。但是,使用ThreadPoolExecutor时,是不可能完成的,因为ThreadPoolExecutor中的Thread无法选择优先执行子任务,需要完成200万个具有父子关系的任务时,也需要200万个线程,显然这是不可行的。

工作窃取算法

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

那么为什么需要使用工作窃取算法呢? 
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

通过实例看一下:

List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    list.add(i);
}
// 统计并行执行list的线程
Set<Thread> threadSet = new CopyOnWriteArraySet<>();
// 并行执行
list.parallelStream().forEach(integer -> {
    Thread thread = Thread.currentThread();
     System.out.println(thread.getName());
    // 统计并行执行list的线程
    threadSet.add(thread);
});
System.out.println("threadSet一共有" + threadSet.size() + "个线程");
System.out.println("系统一个有"+Runtime.getRuntime().availableProcessors()+"个cpu");
}

输出情况是:

...
ForkJoinPool.commonPool-worker-2
ForkJoinPool.commonPool-worker-1
main
threadSet一共有3个线程
系统一个有8个cpu

可以看到创建了3个线程来执行 main代表的是foreach的主线程也算一个线程  而ForkJoinPool.commonPool-worker才是动态调用的线程

这里看到只有3个线程是因为数量级太小了 如果list的元素有1000000个那么会把8个线程都用满

Java 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是ForkJoinPool类型上的一个静态元素,它拥有的默认线程数量等于运行计算机上的处理器数量。

 

5.什么时候用并发流?

较“重”的定时任务

指定线程池
 

 

参考:

对于其原理可以看这里

Java 8 中的 Streams API 详解

java8中的聚合操作

深入浅出parallelStream

 

没有更多推荐了,返回首页