ConcurrentHashMap
是 Java 中一个线程安全的 Map
实现,它在 java.util.concurrent
包中引入于 Java 5。与 Hashtable
不同,ConcurrentHashMap
通过细粒度的锁机制来提供线程安全,允许多个线程同时访问和修改地图中的数据,极大提高了并发性能。
主要特点:
- 线程安全:
ConcurrentHashMap
确保多个线程在没有外部同步的情况下,可以安全地访问和修改数据。 - 分段锁(Java 7 及之前):在 Java 7 及之前的版本中,
ConcurrentHashMap
将 map 分成多个段,每个段有自己的锁,允许不同线程访问不同段的内容,从而减少竞争。 - 锁分条(Java 8 及以后):Java 8 引入了更细粒度的锁,桶级别的锁结合 CAS(比较和交换)操作,极大提升了性能。
- 不允许 null 键和值:与
HashMap
不同,ConcurrentHashMap
不允许使用null
作为键或值。 - 高性能:由于减少了锁的竞争,
ConcurrentHashMap
在多线程环境中比Hashtable
或同步的HashMap
提供更好的性能。 - 原子操作:像
putIfAbsent
、computeIfAbsent
、replace
等方法是原子操作,确保线程安全。 - 弱一致性迭代:迭代器是弱一致性的,在迭代过程中即使有其他线程修改了 map,迭代器也不会抛出
ConcurrentModificationException
异常。 - 良好的可扩展性:由于锁的粒度较小,
ConcurrentHashMap
可以随着线程数的增加而更好地扩展,提供较高的吞吐量。
内部工作原理:
- 结构:
ConcurrentHashMap
内部使用哈希表结构,键通过哈希运算分配到不同的桶(bucket)。如果发生哈希冲突,使用链表或者红黑树(Java 8 及以后)解决冲突。 - 并发级别:Java 7 及以前,构造时可以指定
concurrencyLevel
参数来控制分段数。Java 8 之后,这个参数的影响变得不那么显著,ConcurrentHashMap
会动态调整桶级别的锁。 - Java 8 改进:
- 采用桶级别的锁,通过
synchronized
块和 CAS 操作提高性能。 - 当桶中的元素过多时,将链表转换成红黑树,从而提升查询性能(从 O(n) 降到 O(log n))。
- 支持函数式编程,提供
forEach
、compute
、merge
等方法。
- 采用桶级别的锁,通过
常见方法:
put(K key, V value)
:将指定的值与键关联,如果键已存在,则更新值。putIfAbsent(K key, V value)
:只有当键不存在时,才插入该键值对。get(K key)
:获取与键关联的值。remove(K key)
:移除指定键的键值对。computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
:如果键不存在,使用提供的函数计算并插入新值。forEach(BiConsumer<? super K, ? super V> action)
:迭代所有的键值对。
ConcurrentHashMap
示例代码
下面是几个常见的使用 ConcurrentHashMap
的例子:
示例 1:多线程环境中的基本使用
这个例子展示了如何在多个线程中安全地向 ConcurrentHashMap
中添加和获取数据。
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建 ConcurrentHashMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 模拟多个线程向 map 中添加数据
for (int i = 1; i <= 5; i++) {
final int value = i;
executor.submit(() -> {
map.put("Key" + value, value);
System.out.println(Thread.currentThread().getName() + " added: Key" + value + " -> " + value);
});
}
// 模拟一个线程从 map 中读取数据
executor.submit(() -> {
try {
Thread.sleep(100); // 等待一些插入操作完成
System.out.println("Current map contents: " + map);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 关闭线程池
executor.shutdown();
}
}
解释:
- 多个线程并发地向
ConcurrentHashMap
中插入键值对。 - 另外一个线程读取 map 中的内容。
ConcurrentHashMap
保证了线程安全,因此不需要外部同步机制。
示例 2:使用 putIfAbsent
进行条件插入
这个例子展示了如何使用 putIfAbsent
方法进行条件插入,仅当键不存在时才插入新值。
import java.util.concurrent.ConcurrentHashMap;
public class PutIfAbsentExample {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// 如果键不存在,则插入键值对
map.putIfAbsent("key1", "value1");
System.out.println("After putIfAbsent key1: " + map); // {key1=value1}
// 对同一个键进行第二次 putIfAbsent 插入
map.putIfAbsent("key1", "newValue");
System.out.println("After second putIfAbsent key1: " + map); // 依然是 {key1=value1}
// 插入新的键值对
map.putIfAbsent("key2", "value2");
System.out.println("After putIfAbsent key2: " + map); // {key1=value1, key2=value2}
}
}
解释:
putIfAbsent
方法确保只有当键不存在时才会插入新的键值对。- 第二次对
key1
的插入操作不会覆盖原有的值,保证了原子性。
示例 3:使用 computeIfAbsent
进行懒加载
这个例子展示了如何使用 computeIfAbsent
方法进行懒加载,当键不存在时计算并插入值。
import java.util.concurrent.ConcurrentHashMap;
public class ComputeIfAbsentExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 如果键不存在,则计算值并插入
map.computeIfAbsent("key1", key -> key.length());
System.out.println("After computeIfAbsent key1: " + map); // {key1=4}
// 对相同的键再次调用 computeIfAbsent,不会重新计算
map.computeIfAbsent("key1", key -> 100);
System.out.println("After second computeIfAbsent key1: " + map); // 依然是 {key1=4}
// 为新的键计算值
map.computeIfAbsent("key2", key -> key.length());
System.out.println("After computeIfAbsent key2: " + map); // {key1=4, key2=4}
}
}
解释:
computeIfAbsent
会在键不存在时,根据提供的函数计算并插入值。- 对已存在的键再次调用
computeIfAbsent
时,不会重新计算值。
示例 4:并发迭代
这个例子展示了在一个线程修改 ConcurrentHashMap
的同时,另一个线程迭代其内容。
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentIterationExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);
ExecutorService executor = Executors.newFixedThreadPool(2);
// 线程 1:迭代 map
executor.submit(() -> {
System.out.println("Iterating over map:");
for (var entry : map.entrySet()) {
System.out.println(entry.getKey() + " -> " + entry.getValue());
try {
Thread.sleep(100); // 模拟慢迭代
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 线程 2:修改 map
executor.submit(() -> {
try {
Thread.sleep(50); // 等待迭代开始
map.put("key3", 3);
System.out.println("Added key3 -> 3");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
executor.shutdown();
}
}
解释:
- 一个线程迭代
ConcurrentHashMap
,另一个线程修改 map。 - 由于
ConcurrentHashMap
的迭代器是弱一致性的,修改操作不会抛出ConcurrentModificationException
,但是迭代器可能不会立即反映出新的条目(如key3
)。
总结
ConcurrentHashMap
是一种高效的线程安全 Map
实现,适用于高并发环境。它提供了细粒度的锁机制,使得多个线程可以安全地并发访问和修改数据。常用方法如 putIfAbsent
、computeIfAbsent
和 forEach
提供了原子操作和懒加载的功能。
它非常适合以下场景:
- 多线程并发访问时(例如缓存、计数等)。
- 需要高性能的并发操作时(比
Hashtable
或synchronizedMap
更高效)。 - 不需要
null
键和值时。
但在单线程或对 null
值有需求时,可能不适合使用。