Lambda表达式最佳实践(2)Stream与ParallelStream

Stream是Java8新引入的API,有着广泛的运用

创建一个Stream

Stream创建之后,就不能修改

创建一个空的Stream

Stream<String> streamEmpty = Stream.empty();

一般的,我们用如下这种写法避免空指针异常

public Stream<String> streamOf(List<String> list) {
    return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}

各种创建一个Stream的方法

虽然不常用(因为Stream是一次性的),大家看个热闹就好

Stream of collection

Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();
streamOfCollection.forEach(a -> System.out.println(a));

数组的Stream

String[] arr = new String[]{"a", "b", "c", "d", "e", "f"};
Stream<String> streamOfArrayFull = Arrays.stream(arr);
streamOfArrayFull.forEach(a -> System.out.println(a));
Stream<String> streamOfArrayPart = Arrays.stream(arr, 1, 3);//下标从零开始,startInclusive,endExclusive
streamOfArrayPart.forEach(a -> System.out.println(a));

StreamBuilder

Stream<String> streamBuilder =Stream.<String>builder().add("a").add("b").add("c").build();
streamBuilder.forEach(a -> System.out.println(a));

Stream.generate()

//generate会创建一个无限大的Stream直到内存上限,用limit限制大小
Stream<String> streamGenerated = Stream.generate(() -> "element").limit(10);
streamGenerated.forEach(a -> System.out.println(a));

Stream.iterate()

//从40作为第一个元素开始,每个元素递增2,一共20个元素
Stream<Integer> streamIterated = Stream.iterate(40, n -> n + 2).limit(20);
streamIterated.forEach(a -> System.out.println(a));

原生类型Stream

range(int startInclusive, int endExclusive)
rangeClosed(int startInclusive, int endInclusive)

IntStream intStream = IntStream.range(1, 3);
intStream.forEach(a -> System.out.println(a));
LongStream longStream = LongStream.rangeClosed(1, 3);
longStream.forEach(a -> System.out.println(a));

同时,Random还提供了随机Stream

Random random = new Random();
DoubleStream doubleStream = random.doubles(3);
doubleStream.forEach(a -> System.out.println(a));

Stream of String
通过String获取char的utf-8编码(系统默认编码)Stream:

IntStream streamOfChars = "写作abc".chars();
streamOfChars.forEach(a -> System.out.println(a));//输出:20889,20316,97,98,99

通过正则分割提取Stream

Stream<String> streamOfString =
                Pattern.compile(", ").splitAsStream("a, b, c");
streamOfString.forEach(a -> System.out.println(a));

文件Stream
可以读取文件每一行作为一个Stream

Path path = Paths.get("D:\\test.txt");
Stream<String> streamOfStrings = Files.lines(path);
streamOfStrings.forEach(a -> System.out.println(a));
//可以指定编码读取每一行
Stream<String> streamWithCharset =
        Files.lines(path, Charset.forName("UTF-8"));
streamWithCharset.forEach(a -> System.out.println(a));

Stream 工作流

一般Stream工作流分为三步:源处理,中间处理,结束处理

源处理,例如跳过1个元素:

Stream<String> onceModifiedStream =
  Stream.of("abcd", "bbcd", "cbcd").skip(1);

中间处理:例如转换

Stream<String> twiceModifiedStream =
  stream.skip(1).map(element -> element.substring(0, 3));

结束处理:只能有一个,Stream不可以复用(经过结束处理的)

Stream<String> onceModifiedStream =
                Stream.of("abcd", "bbcd", "cbcd").skip(1);
long count = onceModifiedStream.skip(1).map(element -> element.substring(0, 3)).count();
System.out.println(count);

Stream的使用

一定注意,Stream不可以复用(经过结束处理的),一般Stream的使用如前文所述,分为三步:源处理,中间处理,结束处理

Stream<String> stream = 
  Stream.of("a", "b", "bbb", "bb", "c").filter(element -> element.contains("b"));
Optional<String> anyElement = stream.findAny();
//如果存在,输出该元素,不存在则输出--
System.out.println(anyElement.orElse("--"));

List<String> elements =
  Stream.of("a", "b", "bbb", "bb", "c").filter(element -> element.contains("b"))
    .collect(Collectors.toList());
anyElement = elements.stream().findAny();
System.out.println(anyElement.orElse("--"));
Optional<String> firstElement = elements.stream().findFirst();
System.out.println(firstElement.orElse("--"));

Stream懒调用

工作流中的中间调用是懒调用,意思就是如果没有结束处理,那么中间处理是不会被执行的。例如:

List<String> list = Arrays.asList("abc1", "abc2", "abc3");
list.stream().filter(element -> {
    System.out.println("中间处理");
    return element.contains("2");
});

“中间处理”是不会输出的
只有像这样有结束处理,中间处理才会被调用:

List<String> list = Arrays.asList("abc1", "abc2", "abc3");
list.stream().filter(element -> {
    System.out.println("中间处理");
    return element.contains("2");
}).findFirst();

执行顺序

有时候需要注意下那些处理放在前面,哪些处理放在后面,来提高整体代码效率,例如:

long size = list.stream().map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).skip(2).count();

这个map会被调用3次,但是实际没有必要,因为只用保留一个元素,所以应该将skip放在开头,如下所示:

long size = list.stream().skip(2).map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).count();

所以,一般的,skip(), filter(), distinct() 这些应该放在工作流的开头

Stream 降维

Stream原生api中提供了很多统计功能的接口,例如:count(), max(), min(), sum();假设我们想实现自定义的降维规则,可以用 reduce() 和 collect()这两个接口

reduce()

包括三个参数:

  • identity:accumulator的初始值,并且是在Stream为空时的默认值
  • accumulator:合并逻辑,在每一步合并两个元素,只有最后一步的有用,但是用这个效率不高
  • combiner:accumulator的并发改进版,但必须在ParallelStream的上下文下运行,同时在有identity初始值的情况下结果与accumulator不一样

例子:

OptionalInt reduced =
  IntStream.range(1, 4).reduce((a, b) -> a + b);

reduced = 1 + 2 + 3 = 6

int reducedParams = Stream.of(1, 2, 3)
.reduce(10, (a, b) -> a + b, (a, b) -> {
 log.info("combiner was called");
 return a + b;
});

reducedTwoParams = 10 + 1 + 2 + 3 = 16
由于没有在ParallelStream下,所以combiner没有被调用

int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()
    .reduce(10, (a, b) -> a + b, (a, b) -> {
       log.info("combiner was called");
       return a + b;
    });

reducedParallel = 12 + 13 = 25; 25 + 11 = 36

collect()

List<Product> productList = Arrays.asList(new Product(23, "potatoes"),
  new Product(14, "orange"), new Product(13, "lemon"),
  new Product(23, "bread"), new Product(13, "sugar"));

将Stream转换为List或者Set

List<String> collectorCollection = 
  productList.stream().map(Product::getName).collect(Collectors.toList());

拼接成一个字符串

String listToString = productList.stream().map(Product::getName)
  .collect(Collectors.joining(", ", "[", "]"));

求对象某一数值域平均值

double averagePrice = productList.stream()
    .collect(Collectors.averagingDouble(Product::getPrice));

求对象某一数值域总和

double summingPrice = productList.stream()
  .collect(Collectors.summingDouble(Product::getPrice));

求对象某一数值域统计数据

DoubleSummaryStatistics doubleSummaryStatistics = productList.stream().collect(Collectors.summarizingDouble(Product::getPrice));

输出:DoubleSummaryStatistics{count=5, sum=86.000000, min=13.000000, average=17.200000, max=23.000000}

按照某一对象域分类

Map<Double, List<Product>> collectorMapOfLists = productList.stream().collect(Collectors.groupingBy(Product::getPrice));

结果是:{23.0:[{"name":"potatoes","price":23.0},{"name":"bread","price":23.0}],13.0:[{"name":"lemon","price":13.0},{"name":"sugar","price":13.0}],14.0:[{"name":"orange","price":14.0}]}

按某一条件区分

Map<Boolean, List<Product>> booleanListMap = productList.stream().collect(Collectors.partitioningBy(a -> a.price > 13));

结果是:{false:[{"name":"lemon","price":13.0},{"name":"sugar","price":13.0}],true:[{"name":"potatoes","price":23.0},{"name":"orange","price":14.0},{"name":"bread","price":23.0}]}

collect之后辅以其他操作

Set<Product> unmodifiableSet = productList.stream()
                .collect(Collectors.collectingAndThen(Collectors.toSet(),
                        Collections::unmodifiableSet));

自定义Collector

和reduce有些类似,collector也是有三部分组成:

  • supplier:初始化用的,就是collector返回的结果类型的容器,一般是构造器,例如Collectors.toSet()的Supplier就是HashSet::new
  • accumulator: 用于收集每一个元素,在非parallel环境下使用
  • combiner:用于收集每一个元素,在parallel环境下使用,由于是并发,所以入参是两个已经收集一些元素的容器

例如实现一个LinkedList收集器:

Collector<Product, ?, LinkedList<Product>> toLinkedList =
    Collector.of(() -> {
        System.out.println("supplier is called");
        return new LinkedList<>();
    }, (a, b) -> {
        System.out.println("accumulator is called");
        a.add(b);
    },
    (first, second) -> {
        System.out.println("combiner is called");
        first.addAll(second);
        return first;
    });

stream调用:

LinkedList<Product> collect = productList.stream().collect(toLinkedList);

输出:

supplier is called
accumulator is called
accumulator is called
accumulator is called
accumulator is called
accumulator is called

paralleleStream调用:

LinkedList<Product> collect2 = productList.parallelStream().collect(toLinkedList);

输出:

supplier is called
supplier is called
supplier is called
accumulator is called
supplier is called
accumulator is called
accumulator is called
combiner is called
supplier is called
accumulator is called
accumulator is called
combiner is called
combiner is called
combiner is called

ParallelStream

前面已经多多少少提到了ParallelStream,对于ParallelStream,我们需要知道里面的执行是异步的,并且使用的线程池是ForkJoinPool.common,可以通过设置-Djava.util.concurrent.ForkJoinPool.common.parallelism=N来调整线程池的大小;

ParallelStream的作用

Stream具有平行处理能力,处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作,因此像以下的程式片段:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream()
       .forEach(out::println);  

你得到的展示顺序不一定会是1、2、3、4、5、6、7、8、9,而可能是任意的顺序

parallelStream背后的男人:ForkJoinPool

要想深入的研究parallelStream之前,那么我们必须先了解ForkJoin框架和ForkJoinPool.本文旨在parallelStream,但因为两种关系甚密,故在此简单介绍一下ForkJoinPool,如有兴趣可以更深入的去了解下ForkJoin***(当然,如果你想真正的搞透parallelStream,那么你依然需要先搞透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队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

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

用看forkjion的眼光来看ParallelStreams

上文中已经提到了在Java 8引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行,也就是我们使用了ForkJoinPool的ParallelStream。

Java 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是ForkJoinPool类型上的一个静态元素,它拥有的默认线程数量等于运行计算机上的处理器数量。当调用Arrays类上添加的新方法时,自动并行化就会发生。比如用来排序一个数组的并行快速排序,用来对一个数组中的元素进行并行遍历。自动并行化也被运用在Java 8新添加的Stream API中。

使用ParallelStream需要注意的

在ParallelStream中,如果我们需要收集结果,一种我们是使用collect收集,还有collect无法涵盖的情况例如收集多个结果,这时我们需要使用线程安全的集合收集:

List<Product> resultA = new CopyOnWriteArrayList<>();
List<Product> resultB = new CopyOnWriteArrayList<>();

productList.parallelStream().forEach(product -> {
    if (product.name.length() > 4) {
        resultA.add(product);
    } else {
        resultB.add(product);
    }
});

而且,有时候我们不想只用默认的线程池,而是用我们自己的线程池,这时,上面的代码可以修改成:

List<Product> resultA = new CopyOnWriteArrayList<>();
List<Product> resultB = new CopyOnWriteArrayList<>();

ForkJoinPool forkJoinPool = new ForkJoinPool(5);

forkJoinPool.submit(() -> {
    productList.parallelStream().forEach(product -> {
        if (product.name.length() > 4) {
            resultA.add(product);
        } else {
            resultB.add(product);
        }
    });
}).get();

一定要注意,最后的.get()不能少,因为原来的parallelStream执行,就是同步等待结果完成,程序才会继续执行的

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值