Jakob Jenkov 2024-11-28
java.util.concurrent.ConcurrentMap 简介
java.util.concurrent.ConcurrentMap 接口表示一个能够处理并发访问(插入和获取)的 Java Map。
除了继承自其父接口 java.util.Map 的方法外,ConcurrentMap 还额外提供了一些原子操作方法。
ConcurrentMap 的实现类
由于 ConcurrentMap 是一个接口,你需要使用它的某个具体实现类才能使用它。java.util.concurrent 包中包含以下 ConcurrentMap 接口的实现:
- ConcurrentHashMap
ConcurrentHashMap
ConcurrentHashMap 与 java.util.HashTable 类非常相似,但 ConcurrentHashMap 提供了比 HashTable 更好的并发性能。
- 在读取
ConcurrentHashMap时,不会对整个 Map 加锁。 - 在写入时,也不会锁定整个 Map,而只是锁定内部正在被写入的部分(例如某个“桶”)。
- 另一个区别是:当在迭代
ConcurrentHashMap时,即使 Map 被其他线程修改,也不会抛出ConcurrentModificationException。不过需要注意的是,该Iterator并非设计为多线程共享使用。
ConcurrentMap 使用示例
下面是一个使用 ConcurrentMap 接口的示例,实际使用的是 ConcurrentHashMap 实现:
ConcurrentMap concurrentMap = new ConcurrentHashMap();
concurrentMap.put("key", "value");
Object value = concurrentMap.get("key");
避免“条件竞争”(Slipped Conditions)
尽管 ConcurrentHashMap 的各个方法本身是线程安全的,但如果你使用方式不当,仍然可能遇到并发问题。
下面展示一种错误用法,可能导致 条件竞争(Slipped Conditions):
ConcurrentMap map = new ConcurrentHashMap();
if (!map.containsKey("key1")) {
map.put("key1", "value1");
}
虽然 containsKey() 和 put() 方法各自都是线程安全的,但上述 if 语句整体不是线程安全的。
问题分析
假设有两个线程同时执行上述代码:
- 两个线程几乎同时调用
map.containsKey("key1"); - 两者都得到
false(即 Map 中尚无该 key); - 于是两个线程都进入
if语句体,并分别执行put(); - 第二个线程的
put()会覆盖第一个线程写入的值。
如果插入的值是静态字符串 "value1",问题可能不大;但如果值是根据线程上下文动态计算得出的,就会导致严重错误。
正确做法:使用原子方法
解决方法是使用 ConcurrentMap 提供的原子方法,如 putIfAbsent() 或 computeIfAbsent()。
使用 putIfAbsent()
ConcurrentMap map = new ConcurrentHashMap();
map.putIfAbsent("key1", "value1");
- 该方法仅在 Map 中不存在指定 key 时才插入键值对。
- 整个操作是原子的,确保同一 key 不会被多个线程重复插入。
- 不同 key 的插入操作可能并发进行(取决于内部实现,如是否落在不同“桶”中)。
使用 computeIfAbsent()
ConcurrentMap map = new ConcurrentHashMap();
map.computeIfAbsent("key2", (key) -> {
return "Value for key " + key.toString() + " : " + Thread.currentThread().getName();
});
- 如果指定 key 不存在,则使用提供的函数(lambda 表达式)计算值并插入。
- 对于同一个 absent key,只有一个线程能执行计算并插入,其他线程将直接获取已插入的值,不会重复计算。
注意:lambda 表达式作为第二个参数传入,用于动态生成值。
使用 computeIfPresent()
ConcurrentMap map = new ConcurrentHashMap();
map.computeIfPresent("key2", (key, oldValue) -> {
return "Updated value for " + key;
});
- 仅当指定 key 已经存在时,才使用函数计算新值并更新。
- 如果 key 不存在,则不执行任何操作。
在上面的例子中,由于 Map 初始为空,调用 computeIfPresent("key2", ...) 不会产生任何效果。