记录一个遇到的隐蔽的空指针异常。
公司里的测试同事之前发现项目里有处偶现的空指针异常。
大致的代码是这样的:
private void doWriteTransDetails(List<TransDetailDTO> transDetails) {
Map<String, List<TransDetailDTO>> transDetailsInvestorMap =
transDetails.stream().collect(Collectors.groupingBy(item -> item.getInvestor().getName()));
// 省略
......
}
对于固定的测试集,多次重跑偶现空指针出现在groupingBy中,乍一看推断是item.getInvestor()为null导致getName()出现空指针。但经过debug查看,实际上是偶现item为null的情况,对于固定的测试集,不应该有这样的偶然因素。
测试猜测是否是因为多线程环境造成的线程安全问题?但这个list在最上游是在方法中定义的,封闭在栈中,不可能有多个线程操作它。
经过排查,终于在上游代码中某处发现了元凶。
public void processTransAppLyList(List<TransDetailDTO> transDetails, List<TransApplyDTO> transApplys) {
// 省略
......
transApplys.parallelStream().forEach(trans -> {
TransDetailDTO transDetailDTO = new TransDetailDTO();
// 省略
......
transDetails.add(transDetailDTO);
});
// 下游调用省略
......
}
源头就是因为此处代码用了并行流,并行流是基于Fork/Join框架实现的。直白点说,这里`transApplys.parallelStream().forEach`是一个多线程的并发环境,对一个`transDetails`进行add操作具有不可预期的结果,可能会数组越界,也可能会元素丢失,也可能会部分index的引用为null。
由于此处数据量不是太大,且操作复杂度不高,直接把并行流改为普通流即可修复。
这个比较隐蔽的空指针异常的经验教训是:开发者在考虑用并行流的时候需要再仔细想一下,有必要吗?甚至可以考虑一下有必要用流吗?如果用并行流仍然是需要注意线程安全问题,不要被笼统的`lambda表达式/stream天然是线程安全的`给误导了。