深入理解ConcurrentHashMap源码解析


ConcurrentHashMap是Java中一个非常重要的并发集合类,它提供了线程安全的哈希表实现。其初衷是为了优化同步HashMap,减少线程竞争,提高并发访问效率。随着Java的发展,ConcurrentHashMap在1.7和1.8中经历了显著的变化。以下内容将深入探索这两个版本的区别,同时结合源码和底层实现来进行说明。

1. Java 1.7中的ConcurrentHashMap

在Java 1.7(及之前的版本)中,ConcurrentHashMap采用了分段锁(Segmentation)的概念,其核心是将数据分成一段一段地存储,然后为每一段数据配备一把锁。

1.1 核心实现

在Java 1.7中,ConcurrentHashMap内部维护了一个Segment数组。每个Segment继承自ReentrantLock并且它内部本质上是一个Hash表。这样做的好处是能够减小锁的粒度,提高并发访问的效率。默认Segment 数量为 16,可以通过构造函数来修改默认值。当需要put或get一个元素时,线程首先通过hash定位到具体的Segment,然后在对应的Segment上进行锁定操作。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    // 省略其他属性和方法

    // Segment内部的HashEntry数组
    transient volatile HashEntry<K,V>[] table;

    Segment(float loadFactor, int threshold, HashEntry<K,V>[] tab) {
        this.loadFactor = loadFactor;
        this.threshold = threshold;
        this.table = tab;
    }

    // 其他方法...
}

1.2 写操作

  • 计算Hash和定位Segment: 首先,根据键的hashCode计算hash值,并使用这个hash值找到对应的段(Segment)。每个段内部都有一组桶(Bucket),用于存储键值对。
  • 获取Segment锁:在写入操作开始之前,需要获取目标Segment的锁。这是通过ReentrantLock来实现的,确保同一时间只有一个线程可以操作该Segment内的数据。
  • 计算桶的位置:在获取到Segment锁之后,根据hash值进一步计算具体的桶(Bucket)位置。每个桶用于存储具有相同hash值的键值对。

处理桶中的节点

  • 桶为空:如果桶中还没有节点,则直接创建一个新节点并放入桶中。
  • 桶中已有节点:如果桶中已经存在节点,则需要根据节点的类型(链表或红黑树)进行相应的处理。对于链表,可能需要遍历链表来找到插入位置或替换已有节点;对于红黑树,则需要在树上执行相应的插入或更新操作。
public V put(K key, V value) {
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key);
    int i = (hash >>> segmentShift) & segmentMask;
    return segments[i].put(key, hash, value, false);
}

1.3 读操作

对于读操作,如果没有进行结构修改,可以允许一定程度的并发。如果读操作需要确保最新的数据被读取,可能需要对Segment进行加锁。

2. Java 1.8中的ConcurrentHashMap

Java 8中对ConcurrentHashMap的实现进行了重大的改进。在这个版本中,去掉了Segment的概念,转而使用了CAS操作(Compare-And-Swap)和synchronized关键字配合节点的锁实现高效的并发控制。

2.1 核心实现

在Java 1.8中,ConcurrentHashMap内部主要是由Node数组构成,每个Node包含了一个key-value键值对。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    // 其他方法...
}

2.2 写操作

  • 计算Hash和定位节点:首先,通过键的hashCode计算hash值,并使用这个hash值找到数组中的目标位置(桶)。
  • CAS写入:如果目标位置(桶)为空,即当前没有任何节点,CHM会尝试使用CAS操作来写入新节点。如果CAS成功,则新节点被放置在桶中,写入操作完成。
  • 同步控制:如果目标位置已经有节点存在(无论是链表还是红黑树),那么会根据当前节点的状态来执行不同的操作:
  • 链表:如果桶中的节点是链表,则首先会尝试获取头节点的锁(使用synchronized),然后检查链表或红黑树的状态。在链表的情况下,会遍历链表来找到正确的插入位置,或者替换已有的节点(如果键相同)。
  • 红黑树:如果桶中的节点是红黑树,同样会先获取头节点的锁,然后在树上执行相应的插入或更新操作。
  • 树化:如果链表长度达到某个阈值(TREEIFY_THRESHOLD,默认为8),且数组大小大于MIN_TREEIFY_CAPACITY(默认为64),链表会转换为红黑树。这是为了优化长链表的性能。
  • 表化:如果红黑树的节点数量小于UNTREEIFY_THRESHOLD(默认为6),且数组大小小于MIN_TREEIFY_CAPACITY,红黑树会退化为链表。
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; K fk; V fv;
        // ...
        if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // ...
    }
    return null;
}

2.3 读操作

Java 8的ConcurrentHashMap在读操作上基本不加锁(除非在读操作过程中检测到写操作正在进行),利用volatile关键字的读写内存语义来保证可见性,从而大大提高读操作的并发性。

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;
        }
        // ...
    }
    return null;
}

3. 结构优化

自Java 1.8开始,ConcurrentHashMap内部结构由链表逐渐转化为红黑树,以减少搜索时间。链表在元素数量增加到一定程度时会转换为红黑树结构。

4. 总结

Java 1.7的ConcurrentHashMap通过分段锁实现高并发,但它的并发度受限于Segment的数量。而Java 1.8通过精细化控制,只在必需时进行锁定,显著提升了读写性能,尤其是读操作几乎不受影响,这对于读多写少的场景来说是一个巨大的优化。

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMapJava 中的一个线程安全的哈希表,它是通过分段锁技术实现线程安全。下面对 ConcurrentHashMap源码进行简要解析。 ### 数据结构 ConcurrentHashMap 内部维护了一个 Segment 数组,每个 Segment 都是一个独立的哈希表,而且这些哈希表的数量可以在创建 ConcurrentHashMap 时指定。每个 Segment 内部都是一个类似 HashMap 的数据结构,也就是一个数组加链表的结构。ConcurrentHashMap 中的所有操作都是先定位到对应的 Segment,然后在 Segment 中进行操作。 ### put 方法 ConcurrentHashMap 的 put 方法首先会调用 hash 方法计算键的哈希值,然后根据哈希值找到对应的 Segment。接着会调用 Segment 的 put 方法,这个方法会加锁并且调用内部的 put 方法将键值对放入内部的 HashMap 中。如果 put 时,HashMap 中已经存在了这个键值对,那么就会更新这个键值对的值。最后释放锁。 ### get 方法 ConcurrentHashMap 的 get 方法也是先定位到对应的 Segment,然后调用内部的 get 方法,在内部的 HashMap 中查找键对应的值。由于在查找的过程中没有加锁,所以在多线程的情况下可能会出现一些数据不一致的问题,但是这个问题被认为是可以接受的,因为它不会影响数据的正确性。 ### size 方法 ConcurrentHashMap 的 size 方法也是先定位到对应的 Segment,然后调用内部的 count 方法,这个方法返回的是当前 Segment 中键值对的数量。最后将所有 Segment 中的键值对数量相加得到 ConcurrentHashMap 的大小。 ### 总结 ConcurrentHashMap 是通过分段锁技术实现线程安全的哈希表,它的内部维护了一个 Segment 数组,每个 Segment 都是一个独立的哈希表。ConcurrentHashMap 中的所有操作都是先定位到对应的 Segment,然后在 Segment 中进行操作。在 put 操作的过程中会加锁,而在 get 操作的过程中不会加锁,所以在多线程的情况下可能会出现一些数据不一致的问题,但是这个问题被认为是可以接受的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值