Java8 Parallel Stream:一次线上告警引发的思考

时间:2024-04-02 22:52:49

由于在使用parallelStream()时没有注意并发安全性,导致返回结果中出现null元素,触发了线上告警,上游服务通过RPC调用下游服务时服务超时,而客户端是采用轮询的方式检查接口最新数据,所以每30s就会报一次。
最后查明是由于下游服务在write回流过程中遇到NPE阻断了写入流程导致。
下面贴出事故代码:
Java8 Parallel Stream:一次线上告警引发的思考

由于在并行流过程中使用了非线程安全的ArrayList,底层维护的数据在每次插入数据时,都会将长度增加1,而并发环境下这个非原子操作可能导致在增加了2个单位的索引后,插入一条数据,造成了第1个单位的索引中出现null,这是第一种情况;第二种情况就是缺少数据,也是由于这个操作,在没有增加1的情况下改变了原数据,导致最终的返回集中缺少了元素,造成脏读。

Java8 Parallel Stream:一次线上告警引发的思考


上面是本次事故的全部过程,那么接下来就分析下并行计算的原理和实现过程,以及同类型的并行操作都有哪些?

(1)Java8的ForkJoin

由于parallelStream的实现就是通过Java的ForkJoin,所以先讲它,它具备以下特性:
1)线程池(ForkJoinPool)维护一个线程队列
2)可以分割任务,将父任务拆分成子任务,完全贴合分治思想
3)可以使用工作窃取,即从繁忙线程中窃取任务到空闲任务实现任务划分的均衡

工作流程如下:
Java8 Parallel Stream:一次线上告警引发的思考

工作窃取:
Java8 Parallel Stream:一次线上告警引发的思考

(2)Akka的Actor
每个worker都被称作一个Actor,可以异步的发送和处理数据和消息,用来实现像ForkJoin类似的作业处理流水线。

Java8 Parallel Stream:一次线上告警引发的思考

Actor之间的数据无需共享,就和单线程一样。而且其中的数据也是不可变的,所以可以设计成有状态的。

(3)Go的Goroutine
Go采用轻量级线程和消息传递的模型,因为采用定制版的C编译器,所以性能要比C的coroutine要强。从调度机制上讲,Goroutine的调度完全是协作式的,执行中的goroutine必须遇到阻塞或者显式让出CPU时间片给其他goroutine,并且协程之间没有优先级之分。