2025-04-30 21:51

ConcurrentHashMap在JDK1.7和1.8中的实现方式

王姐姐

Java后端

(71)

(0)

收藏

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条评论

点击登录参与评论