点击上方蓝字 ↑↑ Throwable文摘

关注公众号设置星标,不定时推送高质量原创文章

关注

前提

这篇文章介绍一下日常开发中并行流ParallelStream中隐藏的陷阱,这个问题其实离我们很近,特别是喜欢使用JDK1.8+的流式编程的伙伴,应该会深有感触。

一个故意而为的例子

下面举一个故意而为的例子,实际上应该不会有类似的业务代码:

public class ParallelStreamMain {

    public static void main(String[] args) throws Exception {        List> array = new ArrayList<>();        List item1 = new ArrayList<>();        List item2 = new ArrayList<>();        List target = new ArrayList<>(100);        array.add(item1);        array.add(item2);        array.parallelStream().forEach(x -> {for (int i = 0; i 100000; i++) {                target.add(i);            }        });        System.out.println(target.size());    }}

某一次执行结果为:163913。如果不停地执行这个main方法,最终都会得到一个非200000的结果,这里的问题就在于使用了并行流parallelStream()方法。ParallelStream底层使用了Fork/Join框架实现,也就是应用了线程池ForkJoinPool把并行流中的节点抽象为ForkJoinTask进行计算,背后用到的"任务窃取"等原理这里就不进行展开,只需要明确:

  • ForkJoinPool一般使用Runtime.getRuntime().availableProcessors()(此值一般认为是物理机器的逻辑核心数量)作为并行度(parallelism),简单认为是可并发执行的任务数,并不是工作线程数。
  • 多核机器中,使用ParallelStream在流的节点中的所有操作都相当于在「一个多线程环境中」进行操作,里面的所有操作都会产生不可预期的结果,例如可能会数组越界、添加元素丢失、部分下标index的引用为NULL等等。

一个仿真例子

写这篇文章不是有意为之,其实很早之前笔者曾经遇到一个比较隐蔽的生产故障,其中有一段访问量比较低的代码大致如下:

@Dataprivate static class OrderDTO {

    private String orderId;    private OrderStatus orderStatus;    private BigDecimal amount;    private Long customerId;}

@Dataprivate static class Order {

    private Long id;    private String orderId;    private Integer orderStatus;    private BigDecimal amount;    private Long customerId;    private OffsetDateTime createTime;    private OffsetDateTime editTime;}

public void groupByOrderStatus(Long customerId) {    List orders = orderDao.selectByCustomerId(customerId);    List orderDTOList = new ArrayList<>();    orders.parallelStream().forEach(order -> {        OrderDTO dto = new OrderDTO();        ......        orderDTOList.add(dto);    });    Map> collect             = orderDTOList.stream().collect(Collectors.groupingBy(item -> item.getOrderStatus().getCode()));    ......}

该方法的功能是通过客户ID查询订单列表,然后把订单列表转化为OrderDTO列表,然后再按照订单状态字段进行分组。通过生产日志和测试回归发现,上面的代码段中groupByOrderStatus()方法会偶发空指针异常。

初次出现问题的时候,由于开发者通过Lambda表达式把多处代码压缩为1行,所以从异常栈比较难排查具体发生问题的代码,后面把Lambda表达式以句点起点拆分为多行上线后观察一段时间,最终定位到发生空指针异常的代码段为Collectors.groupingBy(item -> item.getOrderStatus().getCode()),也就是OrderDTO实例中的orderStatus为空对象。这里显然,groupByOrderStatus()方法其实是被封闭在线程栈中调用,本不应该有多个线程去并发修改其中的内容,这里只剩下一个疑点:使用了parallelStream()。后来直接把parallelStream()修改为stream()重新上线,该空指针问题不再复现。

Lambda/Stream其实并不是天然线程安全的,线程安全的前提是它们本身被线程封闭调用,并且不引入多线程环境,像使用了并行流,本质就是引入了多线程环境。所以,在开发功能的时候,需要仔细思考一下:

  1. 是否真的有必要使用Lambda和流式编程?
  2. 是否真的有必要用到并行流?如果使用了并行流,是否需要考虑引入额外的同步机制,例如锁?
  3. 其实并发并不能提高性能,只能提高吞吐量,应该着重去发现和优化性能瓶颈,而不是拼命地把上游改造成并发调用。

笔者有代码洁癖,当时还发现了上面的代码存在映射操作,正确来说应该使用map()函数,而不是forEach()去遍历元素重新装进去另一个列表,方法中的逻辑体现了原开发者其实对Lambda一知半解。

小结

回到最初那个问题,其实使用并行流也可以保证执行结果和预期一致,不过一定需要引入额外的同步机制,例如这里使用「监视器」进行同步:

public class ParallelStreamMain {

    public static void main(String[] args) throws Exception {        List> array = new ArrayList<>();        List item1 = new ArrayList<>();        List item2 = new ArrayList<>();        List target = new ArrayList<>(100);        array.add(item1);        array.add(item2);final Object monitor = new Object();        array.parallelStream().forEach(x -> {synchronized (monitor) {for (int i = 0; i 100000; i++) {                    target.add(i);                }            }        });        System.out.println(target.size());    }}

上面的方法无论执行多少次,最终都只会输出:200000

(本文完 c-1-d e-a-20200710)

system流怎么判断为空_并行流ParallelStream中隐藏的陷阱相关推荐

  1. system流怎么判断为空_面试时被问到单例模式,怎么回答才能让面试官眼前一亮?...

    考虑到马上到来的金九银十的面试季,我给大家介绍一下面试官的必问题,先从单例模式开始,这个设计模式看似简单,想回答得让面试官眼前一亮,还不是那么容易的. 一.什么是单例模式 单例模式是一种常用的软件设计 ...

  2. java 多态判断非空_收藏Java 面试题全梳理

    脚本之家 你与百万开发者在一起 来源 | Java建设者(ID:javajianshe) 作者 |cxuan 如若转载请联系原公众号 Java 基础篇 Java 有哪些特点 并发性的:你可以在其中执行 ...

  3. java 多态判断非空_跳槽涨薪季面试题之java基础(一)

    点击上方[全栈开发者社区]→右上角[...]→[设为星标⭐] 为迎接金九银十跳槽涨薪季,小编汇总了java精编版面试题,大概从java基础.java8特性.多线程.spring.springboot. ...

  4. java8并行流_Java 8:CompletableFuture与并行流

    java8并行流 这篇文章展示了Java 8的CompletableFuture在执行异步计算时如何与并行流进行比较. 我们将使用以下类对长时间运行的任务进行建模: class MyTask {pri ...

  5. delphi中checkcombobox最大长度_并行光信号传输中的信道间传播时间偏差

    1. 并行光传输中的时间偏差Skew 数据通信中100G/400G方案中多以空分复用的方式采用MPO平行光信号传输.在多路光纤平行传输数据时,多路信号总会有一个时间偏差,比如其中一路从发射端到接收端用 ...

  6. java 多态判断非空_重拾JavaSE基础——多态及其实现方式

    今天是比较抽象的多态,希望能给大家带来帮助 主要内容 多态 为什么使用多态 多态的形式 多态的概念 多态的劣势 多态存在的必然条件 类型转换 多态的实现原理 多态的分类 运行时多态的形式 实现原理 常 ...

  7. golang判断结构体为空_如何在Golang中检查结构是否为空?

    golang判断结构体为空 The size of an empty structure is zero in Golang. Here, empty structure means, there i ...

  8. 判断表达式值是否为空_如何在 Python 中判断列表是否为空

    在判断列表是否为空时,你更喜欢哪种方式?决定因素是什么? 在 Python 中有很多检查列表是否是空的方式,在讨论解决方案前,先说一下不同方法涉及到的不同因素. 我们可以把判断表达式可以分为两个阵营: ...

  9. JAVA的Map怎么判断为空_检查Java中的HashMap是否为空

    使用该isEmpty()方法检查HashMap是否为空.让我们首先创建HashMap-HashMap hm = new HashMap(); 现在,添加一些元素-hm.put("Bag&qu ...

最新文章

  1. 使用OpenCV进行标定(Python)
  2. 获取当前应用程序的文件名
  3. UA MATH567 高维统计III 随机矩阵3 集网与覆盖
  4. P4231 三步必杀 二次差分
  5. python正则表达式match,search,find的使用方法
  6. VHDL基础 学习笔记
  7. python网络编程-socketserver模块
  8. 统计 GitHub 评分、收录有意思的 README,GitHub 热点速览
  9. python画曲线图-python画蝴蝶曲线图的实例
  10. 9--Rails数据交互1
  11. Microsoft Visio 2003 对象导入 word 进行编辑
  12. Linux-千兆网卡驱动实现机制浅析
  13. echarts(雷达图和中国地图)
  14. helper java_请教问题,helper类在java中的作用。
  15. SAP 安全证书的导入
  16. 筛选后系列填充_Excel2013里筛选后复制粘贴制作成绩表方法大剖析,3分钟搞定...
  17. myeclipse10 用破解补丁或注册机不能成功破解原因解析
  18. 庆祝EDA夺冠之余,我们来讨论讨论程序员一般想要new一个什么样的对象
  19. STM32 JTAG SWD (PB3 PB4用作普通IO)及启动模式
  20. ise 时钟约束_在ISE下分析和约束时序

热门文章

  1. python10086查询系统_Python获取移动性能指标
  2. mediarecorder添加时间戳_Python脚本实现数据处理(官方实例)和Hive自带时间函数...
  3. BaseActivity与BaseFragment的封装
  4. Java并发编程实战~协程
  5. Java并发编程实战~Balking模式
  6. java 改装电动_java 数组
  7. layer加载的页面偶尔出现空白_Layer-Cake 模式的设计
  8. 格式怎么转换_爱奇艺下载的视频怎么转换成常见的mp4格式?
  9. java jdbc is一个会话_java_JdbcUtilis_单实例
  10. dockerfile拉取私库镜像_还在用Alpine作为你Docker的Python开发基础镜像?其实Ubuntu更好一点...