最新【062期】Java 8 中的 Stream 如此强大,那它的原理是什么吗?,Java组件化架构实践

总结

面试建议是,一定要自信,敢于表达,面试的时候我们对知识的掌握有时候很难面面俱到,把自己的思路说出来,而不是直接告诉面试官自己不懂,这也是可以加分的。

以上就是蚂蚁技术四面和HR面试题目,以下最新总结的最全,范围包含最全MySQL、Spring、Redis、JVM等最全面试题和答案,仅用于参考

一份还热乎的蚂蚁金服面经(已拿Offer)面试流程4轮技术面+1轮HR

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

OK,下面我们先介绍一下Stream接口的相关知识。

BaseStream接口

Stream的父接口是BaseStream,后者是所有流实现的顶层接口,定义如下:

public interface BaseStream<T, S extends BaseStream<T, S>>

extends AutoCloseable {

Iterator iterator();

Spliterator spliterator();

boolean isParallel();

S sequential();

S parallel();

S unordered();

S onClose(Runnable closeHandler);

void close();

}

其中,T为流中元素的类型,S为一个BaseStream的实现类,它里面的元素也是T并且S同样是自己:

S extends BaseStream<T, S>

是不是有点晕?

其实很好理解,我们看一下接口中对S的使用就知道了:如sequential()parallel()这两个方法,它们都返回了S实例,也就是说它们分别支持对当前流进行串行或者并行的操作,并返回「改变」后的流对象。

如果是并行一定涉及到对当前流的拆分,即将一个流拆分成多个子流,子流肯定和父流的类型是一致的。子流可以继续拆分子流,一直拆分下去…

也就是说这里的SBaseStream的一个实现类,它同样是一个流,比如StreamIntStreamLongStream等。

Stream接口

再来看一下Stream的接口声明:

public interface Stream extends BaseStream<T, Stream>

参考上面的解释这里不难理解:即Stream<T>可以继续拆分为Stream<T>,我们可以通过它的一些方法来证实:

Stream filter(Predicate<? super T> predicate);

 Stream map(Function<? super T, ? extends R> mapper);

 Stream flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

Stream sorted();

Stream peek(Consumer<? super T> action);

Stream limit(long maxSize);

Stream skip(long n);

这些都是操作流的中间操作,它们的返回结果必须是流对象本身。

关闭流操作

BaseStream 实现了 AutoCloseable 接口,也就是 close() 方法会在流关闭时被调用。同时,BaseStream 中还给我们提供了onClose()方法:

/** * Returns an equivalent stream with an additional close handler. Close * handlers are run when the {@link #close()} method * is called on the stream, and are executed in the order they were * added. All close handlers are run, even if earlier close handlers throw * exceptions. If any close handler throws an exception, the first * exception thrown will be relayed to the caller of {@code close()}, with * any remaining exceptions added to that exception as suppressed exceptions * (unless one of the remaining exceptions is the same exception as the * first exception, since an exception cannot suppress itself.) May * return itself. * * 

This is an intermediate * operation. * * @param closeHandler A task to execute when the stream is closed * @return a stream with a handler that is run if the stream is closed */

S onClose(Runnable closeHandler);

AutoCloseableclose()接口被调用的时候会触发调用流对象的onClose()方法,但有几点需要注意:

  • onClose() 方法会返回流对象本身,也就是说可以对改对象进行多次调用

  • 如果调用了多个onClose() 方法,它会按照调用的顺序触发,但是如果某个方法有异常则只会向上抛出第一个异常

  • 前一个 onClose() 方法抛出了异常不会影响后续 onClose() 方法的使用

  • 如果多个 onClose() 方法都抛出异常,只展示第一个异常的堆栈,而其他异常会被压缩,只展示部分信息

并行流和串行流

BaseStream接口中分别提供了并行流串行流两个方法,这两个方法可以任意调用若干次,也可以混合调用,但最终只会以最后一次方法调用的返回结果为准

参考parallel()方法的说明:

Returns an equivalent stream that is parallel. May return

itself, either because the stream was already parallel, or because

the underlying stream state was modified to be parallel.

所以多次调用同样的方法并不会生成新的流,而是直接复用当前的流对象。

下面的例子里以最后一次调用parallel()为准,最终是并行地计算sum

stream.parallel()

.filter(…)

.sequential()

.map(…)

.parallel()

.sum();

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万个线程,显然这是不可行的。

Work Stealing原理:

  1. 每个工作线程都有自己的工作队列WorkQueue;

  2. 这是一个双端队列dequeue,它是线程私有的;

  3. ForkJoinTask中fork的子任务,将放入运行该任务的工作线程的队头,工作线程将以LIFO的顺序来处理工作队列中的任务,即堆栈的方式;

  4. 为了最大化地利用CPU,空闲的线程将从其它线程的队列中「窃取」任务来执行

  5. 但是是从工作队列的尾部窃取任务,以减少和队列所属线程之间的竞争;

  6. 双端队列的操作:push()/pop()仅在其所有者工作线程中调用,poll()是由其它线程窃取任务时调用的;

  7. 当只剩下最后一个任务时,还是会存在竞争,是通过CAS来实现的;

用ForkJoinPool的眼光来看ParallelStream

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

比如下面的代码用来遍历列表中的元素并执行需要的操作:

List userInfoList =

DaoContainers.getUserInfoDAO().queryAllByList(new UserInfoModel());

userInfoList.parallelStream().forEach(RedisUserApi::setUserIdUserInfo);

对于列表中的元素的操作都会以并行的方式执行。forEach方法会为每个元素的计算操作创建一个任务,该任务会被前文中提到的ForkJoinPool中的commonPool处理。以上的并行计算逻辑当然也可以使用ThreadPoolExecutor完成,但是就代码的可读性和代码量而言,使用ForkJoinPool明显更胜一筹。

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

值得注意的是,当前执行的线程也会被用来执行任务,所以最终的线程个数为N+1,1就是当前的主线程

这里就有一个问题,如果你在并行流的执行计算使用了_阻塞操作_,如I/O,那么很可能会导致一些问题:

public static String query(String question) {

List engines = new ArrayList();

engines.add(“http://www.google.com/?q=”);

engines.add(“http://duckduckgo.com/?q=”);

engines.add(“http://www.bing.com/search?q=”);

// get element as soon as it is available

Optional result = engines.stream().parallel().map((base) - {

String url = base + question;

// open connection and fetch the result

return WS.url(url).get();

}).findAny();

return result.get();

}

这个例子很典型,让我们来分析一下:

  • 这个并行流计算操作将由主线程和JVM默认的ForkJoinPool.commonPool()来共同执行。

  • map中是一个阻塞方法,需要通过访问HTTP接口并得到它的response,所以任何一个worker线程在执行到这里的时候都会阻塞并等待结果。

  • 所以当此时再其他地方通过并行流方式调用计算方法的时候,将会受到此处阻塞等待的方法的影响。

  • 目前的ForkJoinPool的实现并未考虑补偿等待那些阻塞在等待新生成的线程的工作worker线程,所以最终ForkJoinPool.commonPool()中的线程将备用光并且阻塞等待。

正如我们上面那个列子的情况分析得知,lambda的执行并不是瞬间完成的,所有使用parallel streams的程序都有可能成为阻塞程序的源头, 并且在执行过程中程序中的其他部分将无法访问这些workers,这意味着任何依赖parallel streams的程序在什么别的东西占用着common ForkJoinPool时将会变得不可预知并且暗藏危机。

小结:

1、当需要处理递归分治算法时,考虑使用ForkJoinPool。

2、仔细设置不再进行任务划分的阈值,这个阈值对性能有影响。

3、Java 8中的一些特性会使用到ForkJoinPool中的通用线程池。在某些场合下,需要调整该线程池的默认的线程数量

4、lambda应该尽量避免副作用,也就是说,避免突变基于堆的状态以及任何IO

5、lambda应该互不干扰,也就是说避免修改数据源(因为这可能带来线程安全的问题)

6、避免访问在流操作生命周期内可能会改变的状态

最后

2020年在匆匆忙忙慌慌乱乱中就这么度过了,我们迎来了新一年,互联网的发展如此之快,技术日新月异,更新迭代成为了这个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。

更多JVM面试整理:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

中就这么度过了,我们迎来了新一年,互联网的发展如此之快,技术日新月异,更新迭代成为了这个时代的代名词,坚持下来的技术体系会越来越健壮,JVM作为如今是跳槽大厂必备的技能,如果你还没掌握,更别提之后更新的新技术了。

[外链图片转存中…(img-NMpv8s6x-1715652834264)]

更多JVM面试整理:

[外链图片转存中…(img-XaUWLBn8-1715652834265)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值