Java中ConcurrentHashMap创建唯一Item的线程安全问题及优化
多线程环境下唯一名称Item创建的线程安全问题修复
问题场景
需要创建名称唯一的Item并缓存后返回,运行在多线程环境中,现有代码存在线程安全问题,具体问题标注在代码注释中:
class ItemOperations { private ConcurrentMap<String, Item> store = new ConcurrentHashMap<>(); Item createItem(String name) throws Exception { // 我认为该操作因ConcurrentHashMap而具备线程安全性 if (store.containsKey(name)) { throw new Exception("Already Exists"); } // Item创建是耗时操作 Item newItem = new Item(); // *问题1*:上述代码中,多个线程会创建多个不同的对象,对吗? // putIfAbsent因ConcurrentMap具备线程安全性.. // 失败的线程不会更新值..(若有误请指正) store.putIfAbsent("newItemName", newItem); // *问题2*:不同线程会返回不同的对象,第二个线程返回的是一个临时创建但未存入store的新对象,这会返回错误数据 return newItem; } }
问题解答
关于问题1
是的,多个线程会创建多个不同的Item对象。因为containsKey判断和Item对象创建这两步不是原子操作:当线程A执行完containsKey确认名称不存在后,还没执行putIfAbsent时,线程B也可能完成containsKey判断并得到同样的“不存在”结果,进而创建新的Item对象。
关于问题2
没错,后执行putIfAbsent的线程会操作失败(已有线程完成了对象存入),但它返回的是自己创建的未被缓存的Item对象,导致调用方拿到的对象与缓存中的不一致,属于错误数据。
修复方案
不需要用synchronized包裹整个方法(那样会完全丧失ConcurrentHashMap的并发优势,大幅降低性能),可以利用ConcurrentMap的computeIfAbsent方法,它能保证检查存在性+创建对象+存入缓存整个逻辑的原子性:
class ItemOperations { private ConcurrentMap<String, Item> store = new ConcurrentHashMap<>(); Item createItem(String name) throws Exception { // 先快速检查,利用ConcurrentHashMap get方法的线程安全性 Item existing = store.get(name); if (existing != null) { throw new Exception("Already Exists"); } // 原子性创建并存入:仅当name不存在时才执行lambda创建对象 Item newItem = store.computeIfAbsent(name, key -> new Item()); // 二次校验:避免get之后、computeIfAbsent之前,其他线程已创建该名称的Item if (newItem != store.get(name)) { throw new Exception("Already Exists"); } return newItem; } }
对原有思考的分析
- 用
synchronized包裹整个方法确实能解决问题,但会把并发操作强制转为串行,完全浪费了ConcurrentHashMap的并发性能,不推荐。 - 用
put替代putIfAbsent更不可取:put会覆盖已有的值,不仅产生无用写入,还会导致先存入的正确Item被后续线程的对象覆盖,直接破坏数据唯一性。
内容的提问来源于stack exchange,提问作者HyperVol
相关产品推荐
相关产品推荐

