一 背景
运维在控制台日志中查到了异常堆栈,于是发了个报告给我。看堆栈发现是下面的代码135行数组下标越界,目测就是线程安全问题,就让对应的开发人员把ArrayList换成CopyOnWriteArrayList。下面介绍一下为什么ArrayList会数组下标越界和为什么CopyOnWriteArrayList没有这个问题。
final List<Product> result = new ArrayList<>(); final List<Exception> errorList = new ArrayList<>(); try{ ExecutorService executorService = PostmsgThreadPoolExecutor.getSpareExecutor(); final CountDownLatch countDownLatch = new CountDownLatch(criteriaMap.size()); for(Map.Entry<Long, ProductSKUCriteria> entry : criteriaMap.entrySet()){ executorService.execute(new AbstractInvcProxyThread() { public void runWrapper() { try{ Product[] products = productSKUService.getSimpleProductByCriteria(entry.getValue()); result.addAll(Arrays.asList(products)); }catch(Exception e){ logger.error("getSimpleProductByCriteria异步查询失败", e); errorList.add(e); }finally{ countDownLatch.countDown(); } } }); } countDownLatch.await(15, TimeUnit.SECONDS); }catch(Exception e){ logger.error("getSimpleProductByCriteria异步查询失败", e); throw new BusinessRuntimeException(e); } if(!errorList.isEmpty()) throw new BusinessRuntimeException(errorList.get(0));
二 介绍一下ArrayList的AddAll方法
很简单,核心就四步,先把要添加的集合转成数组,然后把原始数组扩容,接下来是复制元素,最后再更新集合size的属性。但没有加锁
扩容的这个方法,每次调用addall时,只要添加的元素大于一个,就会扩容。他是老数组复制到了一个更大的数组中,然后把新数组的地址指给存储数据的对象。
扩容大小:如果现有元素个数的1.5倍足够存就扩大到1.5倍,否则按照旧+新的总数量扩容。
扩容这个操作也完全没有加锁,这意味着,同一个数组,A线程想扩容10个,B现成想扩容5个,在复制元素的过程中,size没有更新,导致扩容这个操作在多个线程中可以相互覆盖。最后扩容了10个、5个就不一定了。
复制数组的方法是把要添加的数组从最后一个元素之后的位置添加进去,要复制的长度就是我们addall的集合的大小,但实际扩容了多少不确定,如果扩少了,向指定下标的数组填值时,那就会越界。
三 CopyOnWriteArrayList为什么线程安全
写操作全加了ReentrantLock,多个线程时会排队处理