ConcurrentHashMap 1.8详解

1. 存储结构

Java8 的 ConcurrentHashMap 是 Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。针对Node数组中的每个Node节点进行加锁保证线程安全.

2. 初始化 initTable 

sizeCtl变量

  1. sizeCtl 的值为 -1 时:表示哈希表正在初始化。这通常是在创建 ConcurrentHashMap 对象时的初始状态。在初始化过程中,只允许一个线程执行初始化操作,其他线程需要等待。

  2. sizeCtl 的值为负数,但不是 -1 时:表示哈希表正在进行调整大小的操作。具体来说,它值为 -1 - 线程数量,其中线程数量表示参与调整大小操作的活动线程的数量. 在哈希表进行扩容或收缩时,多个线程可能会协同工作确保表的大小调整正确进行。

  3. sizeCtl 的值为 0 时:表示哈希表还没有初始化,即在创建 ConcurrentHashMap 对象时,还没有确定初始的哈希表大小。在这种情况下,通常使用默认的哈希表大小。

  4. sizeCtl 的值为正数时:表示哈希表已经初始化,并且它指示了下一次调整大小的元素数量。一旦哈希表中的元素数量达到 sizeCtl 指定的值,就会触发哈希表的扩容操作。

SIZECTL变量

 SIZECTL 是一个常量,它被设置为 U.objectFieldOffset() 方法的返回值。这个常量的目的是获取 ConcurrentHashMap 类中的 sizeCtl 字段的偏移量。这个偏移量通常用于通过反射或 Unsafe 类来访问对象中的字段,因为它可以帮助确定字段在对象内存中的位置。

让我解释一下代码的含义:

  1. ConcurrentHashMap.class: 这是 ConcurrentHashMap 类的 Class 对象,用于表示该类的元数据。

  2. "sizeCtl": 这是字段的名称,即 ConcurrentHashMap 类中的 sizeCtl 字段的名称。

  3. U.objectFieldOffset: 这是 Unsafe 类的方法,它接受一个类和字段的名称,并返回表示该字段在对象内存中的偏移量。

  4. SIZECTL = U.objectFieldOffset(...): 这行代码的目的是使用 U.objectFieldOffset 方法获取 ConcurrentHashMap 类中的 sizeCtl 字段的偏移量,并将它赋值给 SIZECTL 常量。这个常量可以在以后的代码中用于访问和操作 sizeCtl 字段。

通过这种方式,程序可以绕过正常的访问控制机制来访问对象内部的字段,这在某些情况下是必要的,但要小心使用,因为它涉及到底层内存操作和可能会破坏对象封装性。这通常在特殊情况下,比如高性能数据结构的实现中才会使用。

initTable()方法源码

  从源码中可以发现 ConcurrentHashMap 的初始化是通过自旋和 CAS 操作完成的。里面需要注意的是变量 sizeCtl ,它的值决定着当前的初始化状态。

  1. -1 说明正在初始化
  2. -N 说明有 N-1 个线程正在进行扩容
  3. 0 表示 table 初始化大小,如果 table 没有初始化
  4. >0 表示 table 扩容的阈值,如果 table 已经初始化。

3. put

用于将键值对添加到哈希表中

initTable()方法在 put()方法 中被使用。

执行在一开始没有Node节点数组时

  • 根据 key 计算出 hashcode 。

  • 判断是否需要进行初始化。

  • 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

  • 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

  • 如果都不满足,则利用 synchronized 锁写入数据。

  • 如果数量大于 TREEIFY_THRESHOLD 则要执行树化方法,在 treeifyBin 中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树。


代码:

public V put(K key, V value) {
    return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key 和 value 不能为空
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        // f = 目标位置元素
        Node<K,V> f; int n, i, fh;// fh 后面存放目标位置的元素 hash 值
        if (tab == null || (n = tab.length) == 0)
            // 数组桶为空,初始化数组桶(自旋+CAS)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;  // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 使用 synchronized 加锁加入节点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 说明是链表
                    if (fh >= 0) {
                        binCount = 1;
                        // 循环加入新的或者覆盖节点
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        // 红黑树
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

 4. get

get 流程比较简单,直接过一遍源码。

总结一下 get 过程:

  1. 根据 hash 值计算位置。
  2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
  3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
  4. 如果是链表,遍历查找之。

3. 总结

 Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构是 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值