多线程更新同一mainCode的ConcurrentHashMap值,并发读取是否有问题?
多线程更新与读取ConcurrentHashMap的代码风险分析
多个线程尝试更新同一个mainCode对应的值时,以下代码是否会引发问题?此外,另有一个作业会持续读取securities的值,并在值匹配时执行相应操作。
public class MyObj { private static ConcurrentHashMap<String, ConcurrentHashMap<String, Model>> securities = new ConcurrentHashMap<>(); private static void updateModel(String mainCode, String code, BigDecimal valueA, BigDecimal valueB) { securities.compute(mainCode, (k, v) -> { modifyValue(v, valueA, valueB, code); return v; }); } private static void modifyValue(ConcurrentHashMap<String, Model> v, BigDecimal valueA, BigDecimal ValueB, String code) { v.compute(code, (k, val) -> { val.setCode(code); val.setValueA(valueA); val.setValueB(ValueB); return val; }); } }
存在的问题
1. 空指针异常(NullPointerException)
当首次更新某个mainCode时,securities中不存在该key,compute方法传入的参数v为null。此时调用modifyValue方法,直接对null执行v.compute(code, ...)会触发空指针异常。
2. Model实例的线程安全隐患
假设Model类未做线程安全处理(如下示例):
class Model { private String code; private BigDecimal valueA; private BigDecimal valueB; // 普通setter、getter方法 public void setCode(String code) { this.code = code; } public void setValueA(BigDecimal valueA) { this.valueA = valueA; } public void setValueB(BigDecimal valueB) { this.valueB = valueB; } }
- 可见性问题:如果
Model的字段未用volatile修饰,多线程修改时,持续读取的作业可能无法及时看到最新的字段值。 - 状态不一致:
setCode、setValueA、setValueB是三个独立操作,并非原子性。读作业可能看到部分更新的状态(比如valueA已更新,但valueB还是旧值)。
3. ConcurrentHashMap.compute方法的误用
securities.compute(mainCode, ...)的目的是修改内层ConcurrentHashMap的内容,而非替换内层map本身。当前写法不仅容易引发NPE,还不如使用computeIfAbsent更高效安全——computeIfAbsent可以确保内层map存在时直接返回,不存在则创建新实例。
修正方案
1. 解决空指针问题
将updateModel方法修改为:
private static void updateModel(String mainCode, String code, BigDecimal valueA, BigDecimal valueB) { // 确保内层map存在,不存在则创建 ConcurrentHashMap<String, Model> innerMap = securities.computeIfAbsent(mainCode, k -> new ConcurrentHashMap<>()); modifyValue(innerMap, valueA, valueB, code); }
2. 确保Model的线程安全
可以选择以下方案之一:
- 字段加volatile修饰:让字段的修改对其他线程可见:
class Model { private volatile String code; private volatile BigDecimal valueA; private volatile BigDecimal valueB; // setter和getter不变 } - 使用同步方法:将setter、getter方法改为同步,确保原子性和可见性:
class Model { private String code; private BigDecimal valueA; private BigDecimal valueB; public synchronized void setCode(String code) { this.code = code; } public synchronized void setValueA(BigDecimal valueA) { this.valueA = valueA; } public synchronized void setValueB(BigDecimal valueB) { this.valueB = valueB; } public synchronized String getCode() { return code; } public synchronized BigDecimal getValueA() { return valueA; } public synchronized BigDecimal getValueB() { return valueB; } } - 不可变Model:每次修改创建新的Model实例,彻底避免并发问题:
对应的class Model { private final String code; private final BigDecimal valueA; private final BigDecimal valueB; public Model(String code, BigDecimal valueA, BigDecimal valueB) { this.code = code; this.valueA = valueA; this.valueB = valueB; } // 修改时创建新实例 public Model update(BigDecimal valueA, BigDecimal valueB) { return new Model(this.code, valueA, valueB); } }modifyValue方法调整为:private static void modifyValue(ConcurrentHashMap<String, Model> v, BigDecimal valueA, BigDecimal valueB, String code) { v.compute(code, (k, val) -> { if (val == null) { return new Model(code, valueA, valueB); } return val.update(valueA, valueB); }); }
关于读作业的说明
ConcurrentHashMap的读操作是弱一致性的,即读作业可能看到旧的映射关系,但如果Model本身是线程安全的(比如用volatile或同步方法),读取到的Model字段会是最新的;如果使用不可变Model,读作业看到的要么是旧实例,要么是新实例,不会出现状态不一致的情况。
内容的提问来源于stack exchange,提问作者SmokingTurtle
相关产品推荐
相关产品推荐

