Java ConcurrentHashMap 深度解析
Java ConcurrentHashMap 深度解析
引言
在多线程编程中,HashMap的线程不安全性是一个众所周知的问题。当多个线程同时访问和修改HashMap时,可能会导致数据不一致、无限循环等严重问题。为了解决这个问题,Java提供了多种线程安全的Map实现,其中ConcurrentHashMap是最优秀的选择之一。
什么是ConcurrentHashMap?
ConcurrentHashMap是Java并发包(java.util.concurrent)中的一个线程安全的哈希表实现。它继承了AbstractMap类并实现了ConcurrentMap接口,提供了高效的并发访问能力。
主要特点
- 线程安全:支持多线程并发读写操作
- 高性能:采用分段锁机制,减少锁竞争
- 无锁读取:大部分读操作不需要加锁
- 弱一致性:迭代器具有弱一致性,不会抛出ConcurrentModificationException
ConcurrentHashMap的演进历程
JDK 1.7 版本 - 分段锁机制
在Java 7中,ConcurrentHashMap采用了分段锁(Segment)的设计思想:
// JDK 1.7 的简化结构
static final class Segment<K,V> extends ReentrantLock implements Serializable {transient volatile HashEntry<K,V>[] table;transient int count;transient int modCount;transient int threshold;final float loadFactor;
}
工作原理:
- 将整个哈希表分成多个段(Segment)
- 每个段都是一个独立的哈希表,有自己的锁
- 不同段之间的操作可以并发进行
- 默认分为16个段,支持最多16个线程同时写入
JDK 1.8+ 版本 - CAS + synchronized
Java 8对ConcurrentHashMap进行了重大重构,抛弃了分段锁机制:
// JDK 1.8+ 的核心数据结构
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile int sizeCtl;
新的设计特点:
- 使用Node数组 + 链表/红黑树的结构
- 采用CAS操作进行无锁更新
- 使用synchronized锁定单个桶(bucket)
- 当链表长度超过8时转换为红黑树
核心实现原理
1. 数据结构
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;volatile V val;volatile Node<K,V> next;
}// 树节点
static final class TreeNode<K,V> extends Node<K,V> {TreeNode<K,V> parent;TreeNode<K,V> left;TreeNode<K,V> right;TreeNode<K,V> prev;boolean red;
}
2. PUT操作流程
public V put(K key, V value) {return putVal(key, value, false);
}final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) {Node<K,V> f; int n, i, fh;// 1. 初始化表if (tab == null || (n = tab.length) == 0)tab = initTable();// 2. 桶为空,使用CAS插入else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))break;}// 3. 正在扩容else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);// 4. 桶不为空,使用synchronizedelse {V oldVal = null;synchronized (f) {// 插入链表或树// ...}}}addCount(1L, binCount);return null;
}
3. GET操作流程
public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;int h = spread(key.hashCode());if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {// 1. 检查头节点if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}// 2. 特殊节点处理(树节点或转发节点)else if (eh < 0)return (p = e.find(h, key)) != null ? p.val : null;// 3. 遍历链表while ((e = e.next) != null) {if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null;
}
性能优化技巧
1. 合理设置初始容量
// 根据预期元素数量设置初始容量
int expectedSize = 1000;
int initialCapacity = (int)(expectedSize / 0.75) + 1;
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(initialCapacity);
2. 使用计算方法
// 使用compute系列方法,减少多次查找
map.compute("key", (k, v) -> {if (v == null) {return "new value";} else {return v + " updated";}
});// 原子性累加
map.compute("counter", (k, v) -> (v == null) ? 1 : v + 1);
3. 批量操作
// 使用forEach进行并行遍历
map.forEach(1000, (key, value) -> {// 处理逻辑processKeyValue(key, value);
});// 使用reduce进行并行归约
String result = map.reduce(1000, (key, value) -> key + "=" + value,(s1, s2) -> s1 + "," + s2
);
最佳实践
1. 选择合适的容器
// 读多写少的场景
Map<String, String> readMostly = new ConcurrentHashMap<>();// 写多读少的场景,考虑使用其他并发容器
// 或者使用读写锁保护的HashMap
2. 避免使用size()方法
// 不推荐:size()方法代价较高
if (map.size() > 0) {// 处理逻辑
}// 推荐:使用isEmpty()
if (!map.isEmpty()) {// 处理逻辑
}
3. 正确使用迭代器
// 弱一致性迭代器,不会抛出ConcurrentModificationException
for (Map.Entry<String, String> entry : map.entrySet()) {// 迭代过程中的修改不会立即反映到迭代器中System.out.println(entry.getKey() + " -> " + entry.getValue());
}
与其他Map实现的比较
特性 | HashMap | Hashtable | ConcurrentHashMap |
---|---|---|---|
线程安全 | 否 | 是 | 是 |
null键值 | 支持 | 不支持 | 不支持 |
性能 | 高 | 低(全局锁) | 高(细粒度锁) |
迭代器 | fail-fast | fail-fast | 弱一致性 |
注意事项
1. 复合操作的原子性
// 错误:两个操作之间可能被其他线程修改
if (!map.containsKey(key)) {map.put(key, value);
}// 正确:使用原子性方法
map.putIfAbsent(key, value);
2. 大小计算的准确性
// size()和isEmpty()的结果可能不是完全准确的
// 在高并发环境下,这些方法返回的是近似值
long size = map.mappingCount(); // 推荐使用mappingCount()
总结
ConcurrentHashMap是Java并发编程中不可或缺的工具类。它通过精巧的设计实现了高性能的并发访问,在JDK 8+版本中更是通过CAS和synchronized的组合进一步提升了性能。