一、ConcurrentHashMap介绍
ConcurrentHashMap
是 Java 中的线程安全的哈希表实现,它提供了一种高效的并发访问机制,适用于多线程环境。下面是 ConcurrentHashMap 的一些特性和用法:
- 线程安全性: ConcurrentHashMap 提供了线程安全的操作。 多个线程可以同时读取和修改映射,而不会导致数据不一致或抛出并发修改异常 \color{green} {多个线程可以同时读取和修改映射,而不会导致数据不一致或抛出并发修改异常} 多个线程可以同时读取和修改映射,而不会导致数据不一致或抛出并发修改异常。
- 分段锁: ConcurrentHashMap 内部采用分段锁的机制, 将整个映射分成一系列独立的段,每个段拥有自己的锁。 \color{green}{将整个映射分成一系列独立的段,每个段拥有自己的锁。} 将整个映射分成一系列独立的段,每个段拥有自己的锁。这使得多个线程可以同时访问不同的段,提高了并发性。
- 并发度: 并发度是指 ConcurrentHashMap 中独立锁的个数。默认的并发度为 16,可以在构造方法中指定。更高的并发度可以提高并发性,但也会增加内存开销。
- 无锁读取: 读取操作不需要加锁,因此可以实现高效的并发读取。
- 原子性操作: ConcurrentHashMap 提供了一些原子性的操作方法,如
putIfAbsent
、replace
、remove
等。 - 迭代器弱一致性: ConcurrentHashMap 的迭代器是弱一致的,意味着在迭代过程中,其他线程可能会对映射进行修改,但迭代器不会抛出 ConcurrentModificationException 异常。
二、简单的示例:
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("one", 1);
concurrentMap.put("two", 2);
concurrentMap.put("three", 3);
// 并发安全的修改
concurrentMap.compute("one", (key, value) -> value + 10);
// 获取值
System.out.println(concurrentMap.get("one")); // 输出 11
// 遍历
concurrentMap.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
在实际应用中,ConcurrentHashMap 通常用于需要高并发读写的场景,例如缓存、计数器等 \color{green}{通常用于需要高并发读写的场景,例如缓存、计数器等} 通常用于需要高并发读写的场景,例如缓存、计数器等。
三、与HashMap的区别
ConcurrentHashMap 和 HashMap 是 Java 集合框架中的两个不同的实现,它们之间有一些关键的区别:
1、并发性:
- HashMap 是非线程安全的。在多线程环境下,如果多个线程同时访问和修改 HashMap,可能导致不确定的结果,甚至可能导致数据损坏。
- ConcurrentHashMap 是线程安全的,通过分段锁的机制,它允许多个线程同时读取和写入映射,提高了并发性能。
2、锁机制:
- HashMap 没有提供内置的锁机制,需要外部进行同步控制。
- ConcurrentHashMap 使用分段锁(Segment ocks)的机制,将整个映射分成多个段,每个段都有一个独立的锁。这样在进行写操作时只需锁住对应的段,而不是整个映射。
3、读操作的性能:
- HashMap 在进行读操作时不需要获取锁,但在进行写操作时需要同步控制,可能导致读操作阻塞。
- ConcurrentHashMap 使用分段锁的方式,允许多个线程同时进行读操作,不会阻塞。
4、迭代器的弱一致性:
- HashMap 在迭代器遍历时,如果在迭代过程中有其他线程对集合进行了修改,可能会抛出
ConcurrentModificationException 异常。 - ConcurrentHashMap 提供了弱一致性的迭代器,允许在迭代过程中对集合进行修改。
5、初始容量和负载因子:
- HashMap 默认的初始容量是16,负载因子为0.75。详情可参考【数据结构】哈希表详解,举例说明 java中的 HashMap
- ConcurrentHashMap 的初始容量和负载因子可以通过构造函数进行设置。
总的来说,ConcurrentHashMap
是为了在多线程环境中提供高效的并发访问而设计的,而 HashMap
则适用于单线程环境或者在多线程环境中使用外部同步机制的情况。选择使用哪个取决于具体的应用场景和性能需求。
四、进阶-源码讲解
下面将展示其中一部分关键代码,并对其进行简要解释。
Java 8 版本的 ConcurrentHashMap
中的关键部分:
public class ConcurrentHashMap<K, V> {
// ...
// Segment 数组,每个 Segment 代表一个段
final Segment<K, V>[] segments;
// ...
// Segment 类,实现了 ReentrantLock 接口
static final class Segment<K, V> extends ReentrantLock {
// HashEntry 数组,用于存储键值对
transient volatile HashEntry<K, V>[] table;
// ...
// put 操作
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K, V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
try {
HashEntry<K, V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K, V> first = entryAt(tab, index);
for (HashEntry<K, V> e = first; ; e = e.next) {
if (e == null) {
if (node == null)
node = new HashEntry<K, V>(hash, key, value, first);
else
node.next = first;
setEntryAt(tab, index, node);
break;
}
K k;
if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
return oldValue;
}
}
} finally {
unlock();
}
}
// 其他方法...
static final class HashEntry<K, V> {
final int hash;
final K key;
volatile V value;
final HashEntry<K, V> next;
HashEntry(int hash, K key, V value, HashEntry<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
}
// ...
}
解释:
ConcurrentHashMap 内部维护了一个 Segment
数组,每个 Segment 对应一个段,这些段之间相互独立,各自管理自己的哈希桶。
Segment
类继承了 ReentrantLock
,表示一个可重入锁,每个 Segment
对应一个哈希桶。在对某个段进行操作时,通过 ReentrantLock
来进行加锁,实现对该段的互斥访问。
每个 Segment
中有一个 HashEntry
数组,用于存储键值对。HashEntry
是键值对的节点,采用链表法处理哈希冲突。
put
方法是 ConcurrentHashMap
中进行 put 操作的关键方法。在进行 put 操作时,先通过哈希值找到对应的段,然后在该段的哈希桶中查找键值对。如果找到了相同的键,则更新值;如果没有找到相同的键,则将新的键值对插入到链表的头部。
这只是 ConcurrentHashMap
中的一小部分关键代码,该类的实现非常复杂,包含了诸多细节,如哈希桶的扩容、红黑树的引入等。希望这部分代码能够帮助你理解 ConcurrentHashMap 的基本设计思路。
持续更新中,动动小手,点点关注,后续更精彩~