记因缓存返回引用对象导致的线程安全问题

·

1 min read

背景

前几天处理生产环境问题的时候,遇到一个因为缓存写的不太好,直接返回了引用对象,导致的线程安全问题。

日志信息

日志如下,部分与公司有关的信息已经删掉。

2020-06-01 14:37:49 [ERROR] [task-3] xxx -失败!
java.util.ConcurrentModificationException: null
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) ~[na:1.7.0_80]
    at java.util.ArrayList$Itr.next(ArrayList.java:831) ~[na:1.7.0_80]
2020-06-01 14:37:49 [ERROR] [qtp1073067421-33398] xxx -失败!
java.util.ConcurrentModificationException: null
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859) ~[na:1.7.0_80]
    at java.util.ArrayList$Itr.next(ArrayList.java:831) ~[na:1.7.0_80]

排查过程

两个线程同时抛出一个异常点,在被我删掉的日志中显示这两个线程正在执行同一行代码。

两个线程执行的同一行代码其实是一个foreach遍历,且经我检查,并没有对集合中的对象进行remove的操作。

继续排查发现,这个foreach操作的对象是从一个缓存中获取到的,于是顺着这个缓存开始继续排查。跟着代码调用逻辑发现,有直接往这个缓存返回的list中做add的操作。

检查后发现这个我们系统自己使用ConcurrentHashMap封装的缓存直接返回了引用对象ArrayList,且可读可写。

结论

最后其实结论就是缓存使用不当,导致多线程操作ArrayList对象,一边遍历一边插入新的元素,导致迭代器在做check的时候抛出了一场,出现了线程安全的问题。

解决方案

  1. 返回结果的时候拷贝,不要直接返回引用对象
  2. 使用线程安全的List实现
  3. 还有啥?记一个TODO