多线程并发执行LinkedHashMap的put操作会出现什么问题?
非线程安全LinkedHashMap并发put丢数据的原因
复现代码如下:
Map<Integer, String> hs = new LinkedHashMap<>(); IntStream.range(0,1000).parallel().forEach(value -> hs.put(value, "value")); System.out.println(hs.size());
上述代码并行执行put时最终条目数不足1000,核心原因是LinkedHashMap没有任何并发同步机制,多线程同时执行put操作时会触发三类竞态问题,最终导致数据丢失:
- 写入覆盖问题。LinkedHashMap的put逻辑和HashMap一致,需要先根据key哈希定位到对应的哈希桶,再遍历桶内的链表/红黑树完成节点插入。整个定位+写入的过程没有锁保护,两个线程如果同时定位到同一个哈希桶,各自读取到相同的链表节点状态后分别写入,后写的线程会直接覆盖掉先写线程的插入结果,导致先写入的节点直接丢失。
- size计数丢失。Map的全局size计数是普通int变量,更新操作属于典型的读-改-写非原子操作:两个线程如果同时读取到相同的size值,各自加1后写回内存,两次put最终只会让size增加1,哪怕没有发生节点覆盖,计数本身就会比实际写入次数少。
- 双向链表结构损坏。LinkedHashMap在HashMap的基础上额外维护了一条全局双向链表来保证插入/访问顺序,插入节点时需要同步修改相邻节点的前驱、后继指针。多线程同时修改指针时很容易出现引用覆盖,导致部分节点既不在哈希桶的链路上,也不在全局双向链路上,成为无法被遍历、计数到的“游离节点”,直接丢失。
补充:JDK8之前的HashMap/LinkedHashMap采用头插法实现桶内链表,并发put触发扩容时甚至可能出现链表成环,导致后续get操作死循环;JDK8改为尾插法虽然大幅降低了死循环概率,但依然无法解决数据覆盖、计数丢失、结构损坏的问题,多线程环境下必须使用
Collections.synchronizedMap包装、或者ConcurrentHashMap这类线程安全的Map实现。
内容的提问来源于stack exchange,提问作者Rohit Singh
相关产品推荐
相关产品推荐

