ConcurrentHashMap是Java并发包中提供的线程安全HashMap实现,它在JDK 1.7和JDK 1.8中有显著不同的实现方式。
一、JDK 1.7实现原理
1. 分段锁(Segment)设计
- 将整个哈希表分成多个Segment(默认16个)
- 每个Segment相当于一个独立的HashMap
- 每个Segment有自己的ReentrantLock
- 不同Segment的操作可以并发进行
2. 数据结构
final Segment<K,V>[] segments; static final class Segment<K,V> extends ReentrantLock { transient volatile HashEntry<K,V>[] table; // 其他字段... } static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; }
3. 核心方法实现
- put操作:
1. 计算key的hash值,确定Segment
2. 获取Segment锁
3. 在Segment内部进行类似HashMap的put操作
4. 释放锁
- get操作:
1. 计算key的hash值,确定Segment
2. 不加锁直接访问对应Segment的链表
3. 使用volatile保证可见性
二、JDK 1.8实现原理
1. 重大改进
- 抛弃分段锁设计
- 采用CAS + synchronized实现更细粒度的锁
- 数据结构与HashMap相同(数组+链表/红黑树)
- 锁的粒度从Segment级别降低到桶(bucket)级别
2. 关键数据结构
transient volatile Node<K,V>[] table; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; // 方法... }
3. 核心方法实现
put操作流程:
1. 计算key的hash值
2. 如果table未初始化,则初始化
3. 如果对应桶为空,CAS尝试插入
4. 如果桶不为空:
- 如果正在扩容,帮助扩容
- 否则synchronized锁定桶的头节点
- 遍历链表/红黑树
- 存在则更新,不存在则插入
5. 判断是否需要树化
6. 检查是否需要扩容
get操作流程:
1. 计算key的hash值
2. 找到对应桶
3. 根据hash值和key比较
4. 如果头节点匹配则直接返回
5. 否则遍历链表/红黑树查找
6. 找不到返回null
4. 并发控制机制
1. 初始化控制
private final Node<K,V>[] initTable() { while (table == null) { if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { // 初始化逻辑 } finally { sizeCtl = sc; } } } }
2. 扩容机制
- 多线程协同扩容
- 使用ForwardingNode标记正在迁移的桶
- 每个线程处理一定数量的桶
3. size计算
- 使用CounterCell数组减少竞争
- 采用类似LongAdder的分段计数思想
三、JDK 1.7 vs JDK 1.8对比
| 特性 | JDK 1.7 | JDK 1.8 |
|------|---------|---------|
| 数据结构 | 数组+Segment+链表 | 数组+链表/红黑树 |
| 并发度 | Segment数量固定 | 桶级别锁,并发度更高 |
| 锁机制 | ReentrantLock | CAS + synchronized |
| Hash冲突 | 链表 | 链表+红黑树 |
| 扩容 | 单个Segment扩容 | 整体扩容,多线程协助 |
| 性能 | 中等 | 更高 |
四、关键源码分析
1. putVal方法核心代码
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; if (tab == null || (n = tab.length) == 0) tab = initTable(); else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; } else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { // synchronized锁住桶头节点 synchronized (f) { // 链表/红黑树操作 } } } addCount(1L, binCount); return null; }
2. 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) { if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }
五、使用注意事项
1. 不需要外部同步:ConcurrentHashMap本身就是线程安全的
2. 迭代器弱一致性:迭代时可能反映也可能不反映最新的修改
3. 批量操作:如putAll、clear不是原子性的
4. null值限制:不允许key或value为null
5. size/isEmpty:结果可能不精确,因为并发环境下数据可能变化
六、性能优化点
1. 减少hash冲突:良好的hashCode实现很重要
2. 合理初始容量:避免频繁扩容
3. 并发度设置:在JDK 1.7中可以指定并发度(segment数量)
4. 避免长时间持有锁:复杂的value对象操作可能阻塞其他线程
ConcurrentHashMap通过精细的锁设计和CAS操作,在保证线程安全的同时提供了接近HashMap的性能,是并发环境下Map实现的最佳选择。
0条评论
点击登录参与评论