ConcurrentHashMap源码解析

ConcurrentHashMap是java.util.concurrent包的重要成员, 本文结合JAVA内存模型, 分析ConcurrentHashMap的源码解析.

1. ConcurrentHashMap

在多线程环境下, 一般都是不能直接使用HashMap的, 因为他不是线程安全的. 通常会使用HashTable替代HashMap, 该类基本上所有的方法都采用synchronized进行线程安全的控制, 在高并发的情况下, 每次只有一个线程能够获取对象监视器锁, 这样的并发性能的确不令人满意。另外一种方式通过Collections的Map<K,V> synchronizedMap(Map<K,V> m)将HashMap包装成一个线程安全的Map: SynchronzedMap. 实际上, SynchronzedMap实现依然是采用synchronized独占式锁进行线程安全的并发控制的, 这种方案的性能也是令人不太满意的.

ConcurrentHashMap在JDK 1.5时加入, 底层与HashMap类似, 都采用了数组和链表的结构, 不同的是该类是一个线程安全的Map, 利用了锁分段的思想提高了并发性. 在JDK 1.7中, ConcurrentHashMap为了实现并行访问, 引入了Segment结构, 其最外层不再是一个大的数组, 而是一个Segment数组。每个Segment中包含一个与HashMap数据结构差不多的链表数组, 理论上最大并发度与Segment个数相等。在JDK 1.8中, 为了进一步提高并发性, 摒弃了分段锁方案, 大量使用synchronized以及CAS无锁操作以保证ConcurrentHashMap的线程安全性, 同时为了提高哈希碰撞下的寻址性能, 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N))).

2. 源码分析

2.1 属性

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    // 存储所有元素, 采用懒加载方式, 知道第一次插入数据才进行初始化. 长度总是为2的幂次方
    transient volatile Node<K,V>[] table;
    // 扩容时使用, 平时都是null, 只有在扩容时才为非null
    private transient volatile Node<K,V>[] nextTable;
    // 该属性用于控制table数组的大小, 根据是否初始化和是否正在扩容有几种情况
    // 负数: -1表示正在初始化, -N表示当前正有N-1个线程进行扩容操作.
    // 正数: 如果当前数组为null, 表示正在初始化, 该变量表示新建数组的长度
    //      如果已经初始化, 表示当前table数组可用容量, 也可以理解为临界值(插入节点数超过该临界值就需要扩容)
    //      具体值为数组的长度 * 负载因子(loadFactor)
    // 0: 即数组长度为默认初始值
    private transient volatile int sizeCtl;
    // CAS相关操作需要使用, 具体可以了解CAS相关内容. 本文不再复述.
    private static final sun.misc.Unsafe U;
}

2.2 内部类

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;
    }
}

ConcurrentHashMap的链表结构, 实现了Map.Entry接口, 存放key-value.

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;
    }
}

ConcurrentHashMap的红黑树结构, 继承自Node.

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
}

该类并不会负责包装key-value信息, 而是包装TreeNode节点. 实际上ConcurrentHashMap数组中存放的并不是TreeNode对象, 而是TreeBin对象.

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;
    }
}

该类是在扩容时才会出现的特殊节点, 其key, value, hash全部为null, 并拥有nextTable指针引用的新table数组

2.3 CAS相关操作

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    // 该方法用于获取table数组索引为i的元素
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
    // 该方法利用cas操作设置数组中索引为i的元素
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
    // 该方法用于设置table数组中索引为i的元素
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }
}

2.4 构造函数

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0) 
            throw new IllegalArgumentException();
        // 判断是否超过了允许的最大值,超过了话则取最大值,否则再对该值进一步处理
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
}

ConcurrentHashMap的构造函数并不是很复杂, 其中需要注意的是tableSizeFor方法, 该方法与HashMap中的实现是一致的, 此处不再复述. 另外需要注意的是,调用构造器方法的时候并未构造出table数组(可以理解为ConcurrentHashMap的数据容器),只是算出table数组的长度,当第一次向ConcurrentHashMap插入数据的时候才真正的完成初始化创建table数组的工作。

2.5 初始化数组

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) { // 还没有进行初始化
            if ((sc = sizeCtl) < 0) { // 1. 保证只有一个线程正在进行初始化操作
                Thread.yield(); // lost initialization race; just spin
            } else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        // 2. 计算数组需要的大小
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        // 3. 此处才开始初始化数组 
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // 4. 计算数组的可用大小: 实际大小 * 负载因子(0.75)
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
}

该方法在调用ConcurrentHashMap的put方法时被调用, 用于初始化table数组. 该方法可能存在被多个线程同时调用, 为了保证能够正确初始化,在第1步中会先通过if进行判断,若当前已经有一个线程正在初始化即sizeCtl值变为-1,这个时候其他线程调用Thread.yield()让出CPU时间片。正在进行初始化的线程会调用UnSafe.compareAndSwapInt方法将sizeCtl修改为-1: 即正在初始化的状态。另外还需要注意的是,在第四步中会进一步计算数组中可用的大小(数组实际大小 * 负载因子). 可以看看此处的负载因子是如何计算的, n - (n >>> 2)刚好是n - (1/4)n = (3/4)n.

2.6 扩容

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    // 该方法在putVal方法的最后, addCount方法内进行调用
    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;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        // 新建forwardingNode引用,在之后会用到        
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            // 确定循环中的索引i
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing) {
                    advance = false;
                } else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                } else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            // 将原数组中的元素复制到新数组中去
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    // 处理完成, 重置nextTable, 修改sizeCtl
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            } else if ((f = tabAt(tab, i)) == null) {
                // 当前数组中第i个元素为null,用CAS设置成特殊节点forwardingNode(可以理解成占位符)
                advance = casTabAt(tab, i, null, fwd);
            } else if ((fh = f.hash) == MOVED) {
                // 如果遍历到ForwardingNode节点, 说明这个点已经被处理过了 直接跳过  这里是控制并发扩容的核心
                advance = true; // already processed
            } else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            // 处理当前节点为链表的头结点的情况,构造两个链表,一个是原链表, 另一个是原链表的反序排列
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            } else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0) {
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                } else {
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                                }
                            }
                            // 在nextTable的i位置上插入一个链表
                            setTabAt(nextTab, i, ln);
                            // 在nextTable的i + n的位置上插入另一个链表
                            setTabAt(nextTab, i + n, hn);
                            // 在table的i位置上插入forwardNode节点, 表示已经处理过该节点
                            setTabAt(tab, i, fwd);
                            // 设置advance为true, 返回到上面的while循环中, 就可以执行i--操作
                            advance = true;
                        } else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                    (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }
}

整个扩容操作可以分为两个部分:

第一部分就是构建一个nextTable, 它的容量是原来的两倍.

第二部分就是将原来table中的元素复制到nextTable中. 根据运算得到当前遍历的数组的位置i,然后利用tabAt方法获得i位置的元素再进行判断:

  1. 如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发扩容的关键点
  2. 如果这个位置是Node节点(fh >= 0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上
  3. 如果这个位置是TreeBin节点(fh = -2),也做一个反序处理,并且判断是否需要将红黑树转换为链表, 把处理的结果分别放在nextTable的i和i+n的位置上
  4. 遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的 0.75 倍,完成扩容。
    ConcurrentHashMap

2.7 添加

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    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();
        // 1. 计算hash, 此处可以查看HashMap的实现方式
        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) {
                // 2. 如果当前table未初始化, 进行初始化工作
                tab = initTable();
            } else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 3. table中索引为i的元素为null, 直接添加即可
                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) {
                // 4. 正在扩容
                tab = helpTransfer(tab, f);
            } else {
                // hash碰撞的情况下, 插入到链表或者红黑树中  
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        // 此处需要注意的是, 如果该节点是红黑树, 它的hash值为-2
                        if (fh >= 0) {
                            // 5. 当前为链表, 在链表中插入新的键值对
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                // 如果在链表中找到相同的key, 直接进行覆盖
                                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) {
                                    // 如果直到链表的末尾也没有找到相同的key, 追加到链表的末尾
                                    pred.next = new Node<K,V>(hash, key, value, null);
                                    break;
                                }
                            }
                        } else if (f instanceof TreeBin) {
                            // 6. 当前为红黑树, 插入新的键值对
                            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;
                            }
                        }
                    }
                }
                // 7. 插入完成后, 根据实际大小判断是否需要转换为红黑树
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 8.对当前容量大小进行检查,如果超过了临界值(实际大小 * 负载因子)就进行扩容
        addCount(1L, binCount);
        return null;
    }
}

从整体而言,为了解决线程安全的问题,ConcurrentHashMap使用了synchronzied和CAS进行加锁. 如果ConcurrentHashMap没有出现哈希冲突的情况,每个元素将均匀的分布在哈希桶数组中。当出现哈希冲突的时,将hash值相同的节点构成链表,称为“拉链法”. 另外,在1.8版本中为了防止拉链过长,当链表的长度大于8的时候会将链表转换成红黑树。table数组中的每个元素实际上是单链表的头结点或者红黑树的根节点。当需要插入时, 首先应该定位到要插入的桶,即插入table数组的索引处, 此处使用(n - 1) & hash进行计算即可得到索引.

如果当前节点不为null, 并且为特殊节点(ForwardingNode, 该节点的hash值为MOVED; -1)的话,就说明当前concurrentHashMap正在进行扩容.

接下来, 当前节点不为null也不为特殊节点, 并且hash值大于0的情况下, 说明当前节点为所有hash一致的元素组成的链表的头节点. 通过synchronized的方式进行加锁以实现线程安全性, 将键值对插入到链表中. 之后就是对红黑树的添加, 可以看到if判断中的条件是f instanceof TreeBin, 说明TreeBin是对TreeNode的进一步封装, 对红黑树进行操作的时候针对的是TreeBin而不是TreeNode. 与添加链表的逻辑相同, 如果key在红黑树中已经存在就覆盖旧值,否则就向红黑树追加新节点。

在插入方法的最后, 会对当前链表大小进行调整. 当前链表节点个数大于等于8(TREEIFY_THRESHOLD)的时候,就会调用treeifyBin方法将链表转换成红黑树。

2.8 删除

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        // 计算hash  
        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) {
                // 存储的hash值与传入key的hash相同.
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    // key完全相同, 直接返回即可 
                    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;
    }
}

整个获取的方法比较简单, 直接看注释即可.

3. 总结

相对于JDK 1.7, JDK 1.8中的ConcurrentHashMap修改的比较多, 比如放弃Segment采用锁node方式减少锁的粒度等等. 关于更多1.7版本与1.8版本中ConcurrentHashMap的实现对比, 可以参考这里.

本文分析的代码只是ConcurrentHashMap的一部分, 更多的源码分析请参考其他文章进行阅读.

posted on 2021-02-22 20:59  annwyn  阅读(125)  评论(0)    收藏  举报

导航