初始化后无修改的HashMap并发读出现元素丢失及锁重复获取问题
问题根因
- **即使没有后续写操作,普通
HashMap也不保证并发场景下的读安全,你的问题核心来自两方面:
- 安全发布缺失:你当前的
lockMap没有使用final修饰,也没有做任何安全发布处理。根据Java内存模型的规定,构造函数内对非final字段的赋值操作,不保证对其他线程立即可见。也就是说其他线程可能看到一个半初始化的HashMap:比如内部哈希表数组还未完成赋值,或者部分entry数据没有同步到主存,导致并发get操作时要么找不到已存在的key,要么返回null,甚至拿到错乱的entry对象。 - 代码逻辑漏洞:你在
containsKey判断key不存在后仅打印日志,但没有终止流程,后续依然执行lockMap.get(ii).tryLock()会直接触发NPE。同时你两次调用get分别获取锁、执行解锁,也存在两次读取状态不一致的风险。
- 你观测到的两次
hello连续打印的现象,就是因为不同线程读取到HashMap的错乱状态,拿到了不同的ReentrantLock实例,因此同时加锁成功进入临界区;后续找不到key、NPE的异常,就是半初始化状态的HashMap读取返回null导致的。
修复方案
- 优先替换为
ConcurrentHashMap:Java 8及以上版本的ConcurrentHashMap读操作全程无锁,仅写操作时才会加细粒度锁,你这种初始化后完全无写操作的场景下,性能和普通HashMap几乎没有差异,你担心的性能开销完全不存在。
- 优先替换为
- 若不使用
ConcurrentHashMap,必须做安全发布:将lockMap声明为final类型,构造函数初始化完成后用Collections.unmodifiableMap()包裹生成不可变视图,保证内部状态对所有线程可见。
- 若不使用
- 修复代码逻辑漏洞:将多次查询优化为单次
get存入局部变量,key不存在时直接终止流程,同时将解锁操作放到finally块中避免锁泄漏:
- 修复代码逻辑漏洞:将多次查询优化为单次
void runConcurrently() { int ii = 10; ReentrantLock lock = lockMap.get(ii); if (lock == null) { log.error("lock id is not found in the lockMap " + ii); return; } boolean locked = lock.tryLock(); if (!locked) { return; } try { runCriticialSection(); } finally { lock.unlock(); } }
内容的提问来源于stack exchange,提问作者goozoo
相关产品推荐
相关产品推荐

