ConcurrentHashMap的源码解读

jdk1.8中ConcurrentHashMap的源码解读

ConcurrentHashMap是Java中一个非常重要的并发容器,它提供了一个高效的线程安全的哈希表,支持多个线程同时进行读写操作,而不需要加锁。在jdk1.8中,ConcurrentHashMap的实现发生了很大的变化,本文将从以下几个方面来分析其源码:

  • 基本结构和属性
  • put方法
  • get方法
  • resize方法

基本结构和属性

ConcurrentHashMap的基本结构如下:

java 复制代码
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    private static final long serialVersionUID = 7249069246763182397L;

    // 最大容量,2^30
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    // 默认初始容量,16
    private static final int DEFAULT_CAPACITY = 16;

    // 默认负载因子,0.75
    private static final float LOAD_FACTOR = 0.75f;

    // 并发级别阈值,当并发线程数超过这个值时,扩容时会使用多线程
    private static final int CONCURRENCY_LEVEL = 16;

    // 数组大小掩码,用于计算数组索引
    private static final int HASH_BITS = 0x7fffffff; 

    // 转移节点的hash值,表示当前节点是一个转移节点
    static final int MOVED     = -1; 

    // 树形节点的hash值,表示当前节点是一个树形节点
    static final int TREEBIN   = -2; 

    // 红黑树节点的hash值,表示当前节点是一个红黑树节点
    static final int RESERVED  = -3; 

    // 最小树形化阈值,当链表长度超过这个值时,会转换为红黑树
    static final int MIN_TREEIFY_CAPACITY = 64;

    // Node类,表示一个普通的链表节点
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

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

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        // 返回当前节点的后继节点,如果是转移节点,则帮助转移后返回后继节点
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    int eh; K ek;
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                    if (eh < 0)
                        return e.find(h, k);
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

    // ForwardingNode类,表示一个转移节点,用于扩容时的数据迁移
    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }

        Node<K,V> find(int h, Object k) {
            // loop to avoid arbitrarily deep recursion on forwarding nodes
            outer: for (Node<K,V>[] tab = nextTable;;) {
                Node<K,V> e; int n;
                if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                    return null;
                for (;;) {
                    int eh; K ek;
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                    if (eh < 0) {
                        if (e instanceof ForwardingNode) {
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer;
                        }
                        return e.find(h, k);
                    }
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }
    }

    // TreeBin类,表示一个树形节点,用于存储红黑树的根节点和锁对象
    static final class TreeBin<K,V> extends Node<K,V> {
        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;
        // values for lockState
        static final int WRITER = 1; // set while holding write lock
        static final int WAITER = 2; // set when waiting for write lock
        static final int READER = 4; // increment value for setting read lock

        // 构造方法,将链表转换为红黑树
        TreeBin(TreeNode<K,V> b) {
            super(TREEBIN, null, null, null);
            this.first = b;
            TreeNode<K,V> r = null;
            for (TreeNode<K,V> x = b, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (r == null) {
                    x.parent = null;
                    x.red = false;
                    r = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = r;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            r = balanceInsertion(r, x);
                            break;
                        }
                    }
                }
            }
            this.root = r;
            assert checkInvariants(root);
        }

        // 省略其他方法...
    }

    // TreeNode类,表示一个红黑树节点,继承自Node类
    static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K,V> next,
                 TreeNode<K,V> parent) {
            super(hash, key, val, next);
            this.parent = parent;
        }

        Node<K,V> find(int h, Object k) {
            return findTreeNode(h, k, null);
        }

        // 省略其他方法...
    }

    // transient关键字表示该属性不会被序列化
    transient volatile Node<K,V>[] table;

    // 下一个扩容的阈值,等于容量乘以负载因子
    private transient volatile int sizeCtl;

    // 记录当前正在进行扩容的线程数
    private transient volatile int transferIndex;

    // 记录当前正在进行扩容的线程的栈
    private transient volatile TransferStack<K,V> transferStack;

    // 记录当前map中元素的个数,使用LongAdder类来保证原子性和高效性
    private transient volatile long baseCount;

    // 记录当前map

put方法

put方法是ConcurrentHashMap的核心方法之一,它用于向map中插入或更新一个键值对。put方法的源码如下:

java 复制代码
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;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // 初始化table
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // CAS成功,直接插入
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // 扩容中,帮助转移
        else {
            V oldVal = null;
            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; // 找到相同的key,更新value
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) { // 没有找到相同的key,插入新节点
                                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; // 找到相同的key,更新value
                            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;
}

从上面的代码可以看出,put方法的大致流程如下:

  • 首先,判断key和value是否为空,如果为空则抛出空指针异常。
  • 然后,计算key的hash值,并根据hash值找到对应的数组索引i。
  • 接着,判断数组索引i处是否有节点存在,如果没有,则使用CAS操作尝试插入一个新的节点。
  • 如果有节点存在,则判断该节点的hash值是否为MOVED,如果是,则说明当前正在进行扩容,那么就调用helpTransfer方法来帮助转移数据,并重试。
  • 如果该节点的hash值不为MOVED,则判断该节点是链表节点还是树形节点。如果是链表节点,则遍历链表,查找是否有相同的key存在,如果有,则更新value,如果没有,则插入新的节点。如果是树形节点,则调用putTreeVal方法,按照红黑树的规则,查找或插入新的节点。
  • 在遍历或插入的过程中,需要同步锁定头节点,以防止其他线程的干扰。同时,需要记录链表或树形的长度,以便后续判断是否需要树形化或扩容。
  • 最后,如果插入了新的节点,则调用addCount方法来增加元素个数,并检查是否需要扩容。如果更新了旧的节点,则返回旧的value。

从这个流程可以看出,put方法尽量减少了锁的使用,只有在链表或树形节点的情况下才会锁定头节点,而且只锁定一个节点,不影响其他索引处的操作。同时,put方法也利用了CAS操作来实现无锁插入和扩容,提高了并发效率。另外,put方法还支持链表转换为红黑树的功能,以提高查询性能。

get方法

get方法是ConcurrentHashMap的另一个核心方法,它用于从map中获取一个键对应的值。get方法的源码如下:

java 复制代码
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;
}

从上面的代码可以看出,get方法的大致流程如下:

  • 首先,判断key是否为空,如果为空则抛出空指针异常。
  • 然后,计算key的hash值,并根据hash值找到对应的数组索引i。
  • 接着,判断数组索引i处是否有节点存在,如果没有,则返回null。
  • 如果有节点存在,则判断该节点的hash值是否等于key的hash值,如果是,则判断该节点的key是否等于key,如果是,则直接返回该节点的value。
  • 如果该节点的hash值不等于key的hash值,则判断该节点的hash值是否小于0,如果是,则说明该节点是转移节点或树形节点。那么就调用find方法来查找对应的value。
  • 如果该节点的hash值大于0,则说明该节点是链表节点。那么就遍历链表,查找是否有相同的key存在,如果有,则返回对应的value。

从这个流程可以看出,get方法是一个完全无锁的操作,它不需要同步任何数据结构,也不需要CAS操作。它只是简单地根据hash值和key来查找对应的value。这样可以保证get方法的高效性和一致性。

resize方法

用于在元素个数超过阈值时,将原来的数组扩大为原来的两倍,并将原来数组中的数据迁移到新数组中。resize方法的源码如下:

java 复制代码
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        transferStack = new TransferStack<K,V>();
        transferIndex = n;
        advanceCount = new LongAdder();
    }
    int nextn = nextTab.length;
    TransferStack<K,V> fs = transferStack;
    TransferStack.Node snode;
    int sc; // signal
    while (true) {
        if ((snode = fs.pop()) == null) { // pop transfer task
            if ((sc = sizeCtl) < 0)
                break; // end of resize
            if (sc > 0 && fs.push(new TransferStack.Node(sc))) {
                sizeCtl = sc - 1; // claim new task
                continue;
            }
        }
        else if (snode instanceof TransferStack.ForwardingNode) {
            continue; // already processed
        }
        else {
            int fn = snode.fence, hn = snode.hash, origin, index;
            if ((index = hn + stride) >= n)
                index -= n;
            if ((origin = hn) < index || index < 0)
                index += n;
            boolean empty = true;
            for (int i = origin;;) { // traverse and process
                Node<K,V> f, e; int h; K k; V v;
                if ((f = tabAt(tab, i)) == null)
                    advanceCount.increment();
                else if ((h = f.hash) < 0) {
                    if (f instanceof ForwardingNode) {
                        if (((ForwardingNode<K,V>)f).nextTable == nextTab)
                            advanceCount.increment();
                        else
                            setTabAt(tab, i, new ForwardingNode<K,V>(nextTab));
                    }
                    else if (f instanceof TreeBin)
                        ((TreeBin<K,V>)f).split(this, tab, i, n);
                    else { // list split
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        do {
                            next = f.next;
                            if ((f.hash & n) == 0) {
                                if ((loTail == null))
                                    loHead = f;
                                else
                                    loTail.next = f;
                                loTail = f;
                            }
                            else {
                                if ((hiTail == null))
                                    hiHead = f;
                                else
                                    hiTail.next = f;
                                hiTail = f;
                            }
                        } while ((f = next) != null);
                        setTabAt(nextTab, i, loHead);
                        setTabAt(nextTab, i + n, hiHead);
                        setTabAt(tab, i, new ForwardingNode<K,V>(nextTab));
                        advanceCount.increment();
                    }
                }
                else {
                    boolean added = false;
                    for (Node<K,V> e1;;) {
                        K ek; V ev; int eh; V enew; Node<K,V> pred, p, q;
                        if ((e1 = e.next) == null ||
                            (ek = e1.key) == null ||
                            !(ek instanceof Comparable)) {
                            break; // give up on list
                        }
                        if ((eh = spread(ek.hashCode())) <= h)
                            break; // end of run
                        pred = e; e1.hash |= MOVED; e1.key = null; e1.val = null;
                        for (p = e;;) {
                            if ((q = p.next) == null ||
                                (k = q.key) == null ||
                                !(k instanceof Comparable)) {
                                p.next = e1; e1.next = q;
                                added = true;
                                break;
                            }
                            if ((ev = q.val) == null) {
                                if (q instanceof TreeBin)
                                    break; // leave to tree split
                                if (q.casVal(null, enew = f.apply(ek, ev))) {
                                    added = true;
                                    break;
                                }
                            }
                            else if ((q.hash & MOVED) != 0 ||
                                     !ek.equals(k))
                                break; // end of list
                            else if (q.casVal(ev, enew = f.apply(ek, ev))) {
                                added = true;
                                break;
                            }
                            p = q;
                        }
                        if (added)
                            break;
                    }
                    if (!added) { // try to append
                        Node<K,V> r, p;
                        if ((r = new ReservationNode<K,V>()) != null) {
                            synchronized (r) {
                                if ((p = tabAt(tab, i)) == f) {
                                    int rs = resizeStamp(n);
                                    Node<K,V> g = new ReservationNode<K,V>();
                                    g.next = r; r.next = f;
                                    setTabAt(tab, i, g);
                                    for (int j = 0; j < n; ++j) {
                                        while ((p = tabAt(tab, j)) != null &&
                                               p.hash < 0) {
                                            Thread.yield(); // wait for resize
                                        }
                                    }
                                    transfer(tab, nextTab);
                                    return nextTab;
                                }
                            }
                        }
                    }
                }
                if (!empty)
                    advanceCount.increment();
                if (i == index || !empty)
                    break;
                if ((i += stride) >= n)
                    i -= n;
            }
            fs.addCount(1L, -1);
        }
    }
}

从上面的代码可以看出,resize方法的大致流程如下:

  • 首先,判断是否需要初始化新的数组,如果是,则创建一个长度为原来两倍的数组,并初始化一些辅助变量,如transferStack、transferIndex和advanceCount。
  • 然后,从transferStack中弹出一个转移任务,如果没有,则尝试从sizeCtl中获取一个新的任务,并将其压入transferStack中。
  • 接着,根据转移任务的范围,遍历原数组中的节点,并将其迁移到新数组中。迁移的过程中,需要判断节点的类型,如果是普通节点,则按照hash值的最高位来分配到新数组的两个位置。如果是转移节点,则直接跳过。如果是树形节点,则调用split方法来分割红黑树。如果是链表节点,则按照链表的顺序来分割链表。
  • 在迁移的过程中,需要将原数组中的节点替换为一个ForwardingNode,表示该位置已经被转移。同时,需要使用CAS操作来保证原子性和一致性。另外,还需要使用advanceCount来记录转移的进度和状态。
  • 最后,如果所有的转移任务都完成了,则返回新的数组,并更新相关的属性。

从这个流程可以看出,resize方法是一个非常复杂和精妙的方法,它利用了多线程、CAS操作、无锁编程、红黑树等多种技术,实现了高效和安全的扩容功能。它不仅保证了扩容过程中不影响其他线程的读写操作,而且还能平衡扩容的负载和速度。它是ConcurrentHashMap性能优异的重要保证。

相关推荐
wm104318 分钟前
java web springboot
java·spring boot·后端
smile-yan20 分钟前
Provides transitive vulnerable dependency maven 提示依赖存在漏洞问题的解决方法
java·maven
老马啸西风21 分钟前
NLP 中文拼写检测纠正论文-01-介绍了SIGHAN 2015 包括任务描述,数据准备, 绩效指标和评估结果
java
Earnest~24 分钟前
Maven极简安装&配置-241223
java·maven
皮蛋很白27 分钟前
Maven 环境变量 MAVEN_HOME 和 M2_HOME 区别以及 IDEA 修改 Maven repository 路径全局
java·maven·intellij-idea
青年有志29 分钟前
JavaWeb(一) | 基本概念(web服务器、Tomcat、HTTP、Maven)、Servlet 简介
java·web
上海研博数据33 分钟前
flink+kafka实现流数据处理学习
java
KpLn_HJL34 分钟前
leetcode - 2139. Minimum Moves to Reach Target Score
java·数据结构·leetcode
小扳2 小时前
微服务篇-深入了解 MinIO 文件服务器(你还在使用阿里云 0SS 对象存储图片服务?教你使用 MinIO 文件服务器:实现从部署到具体使用)
java·服务器·分布式·微服务·云原生·架构
龙少95432 小时前
【深入理解@EnableCaching】
java·后端·spring