并发编程第十天----HashMap 与 ConcurrentHashMap 源码解析

404 阅读16分钟

HashMap


HashMap 数据结构

数组查找效率高,增删效率慢,链表增删效率高,查找效率慢 。

hash 表综合了他们的优点,采用键值对形式存储,能根据键快速地取到相应的值,底层结构(JDK 1.8后)是数组+链表+红黑树。

本文分析的源码基于 JDK 1.8 ,数据存储在 Node 节点中。

Node 节点有四个成员变量

final int hash;   // 与节点在数组中的下标有关
final K key;  // 键
V value;  // 值
Node<K,V> next;  // 指向当前桶中的下个节点

成员变量分析

// 默认初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

// 装填因子,size > 容量 * 装填因子 时,触发扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//  桶中的节点数量大于8时,链表将尝试转成红黑树
static final int TREEIFY_THRESHOLD = 8;  

//  桶中的节点数量小于6时,树转链表
static final int UNTREEIFY_THRESHOLD = 6;  

// 链表转红黑树要求的数组的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;

// 存储节点的数组,每个数组空间称为一个桶
transient Node<K,V>[] table;

//  表示数组中元素的个数
int size; 

// 阈值,size 达到该值时扩容
int threshold 

// 装填因子
final float loadFactor;  

//迭代器 failfast
int modcount; 

注释写的很清楚了


构造函数

    // 无参构造函数
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
    
    // 有参构造函数
    public HashMap(int initialCapacity, float loadFactor) {
    
        // ...,略过验证数据
        
        this.loadFactor = loadFactor; // 初始化装填因子
        this.threshold = tableSizeFor(initialCapacity);  // 初始化阈值
    }

tableSizeFor() 通过位移算法,返回大于等于给定目标数值的最小二次幂。比如 传的参数是 9,返回的就是 16;传的参数是 17,返回的就是 32。

为什么要这样设定呢? 在后面的初始化函数中,table 数组的初始大小就是阈值的大小,可以保证 talbe 的大小为 2 的 n 次幂。这又有什么好处?


为什么初始容量是 2 的 n 次幂

后面为元素计算其在数组中的下标时,是通过 hash & (length-1) 计算的。

这个算法其实就是取模运算,但是计算机中,直接求余效率很低,远不如位移运算,为了保证唯一运算和取模运算得到的结果一直,数组容量必须是 2 的 n 次幂。

并且数组容量如果不为 2 的 n 次幂,计算出来的索引下标特别容易冲突,可以减少 hash 碰撞


hash()

通过 hash() 算法,元素可以定位到自己该存到哪个桶。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // 异或运算,相同为 0,不同为 1
    }

为什么要选用这个 hash 算法,有什么好处?

混合原始哈希码的高位和低位,以此来加大低位的随机性,减少哈希碰撞


put()

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

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0) // 数组为空,初始化
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)  // 计算下标
            tab[i] = newNode(hash, key, value, null); // 如果该下标处没有元素,直接赋值,否则发生 hash 冲突
        else {  // 发生了 hash 冲突
            Node<K,V> e; K k;   
            if (p.hash == hash &&   // 判断 key 是否重复
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode) // 红黑树
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {   // 链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null); // 插入节点
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 桶中的节点数量为8时,尝试将链表转红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&   // 判断 key 是否重复
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // 表明有 key 重复,新加入的节点值会覆盖旧的节点值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold) // 数组大小大于阈值,进行扩容
            resize();
        return null;
    }

存入 hash 表中的元素的下标计算公式 :(n - 1) & hash,其中 n 是数组的长度。

e 不为空时,说明 key 重复了,新加入的节点值会覆盖旧节点的值。

链表转红黑树条件

桶中的节点数量大于等于 8 时,链表不一定能转化成红黑树。

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            ....
        }
    }

如果当前 table 的大小小于 64 ( 此时数组容量过小,很容易发生碰撞,优先考虑扩容 ) 时,只会发生扩容。

所以链表转红黑树的条件: 桶中的节点数量大于等于 8 && table 的大小大于 64


为什么桶中节点数超过 8 才转红黑树

红黑树的查找性能比链表的查找性能更高,为什么不一开始就直接用红黑树呢?

源码 175 行的注释对这个问题进行了分析: 树节点的大小大约是普通节点大小的两倍,,并且由泊松分布频率表可以看出,桶的长度超过8的概率非常非常小,为了综合时间和空间的平衡,才选择了 8。


resize()


初始化

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;   // 旧阈值
        int newCap, newThr = 0; 
        if (oldCap > 0) {
           // 扩容
        }
        else if (oldThr > 0) // 有参初始化
            newCap = oldThr;    // 旧阈值赋给 newCap
        else {               // 无参初始化
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor; // 计算新阈值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            //  扩容
        }
        return newTab;
    }

初始化时,数组容量的大小就是旧阈值的大小。


扩容

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) { // 扩容
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  // 新容量为旧容量的两倍
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // 新阈值为旧阈值的两倍
        }
        
        // 初始化 ...
        
        threshold = newThr;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) { // 处理每个桶中的节点
                Node<K,V> e;
                if ((e = oldTab[j]) != null) { 
                    oldTab[j] = null;   
                    if (e.next == null)   // 桶中只有一个节点
                        newTab[e.hash & (newCap - 1)] = e; // 直接计算新下标
                    else if (e instanceof TreeNode)  // 红黑树扩容,有兴趣的自己看看
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {  // 桶中有多个节点,并且是链表形式
                        Node<K,V> loHead = null, loTail = null;  // 指向索引位置不用变的节点
                        Node<K,V> hiHead = null, hiTail = null;  // 指向索引位置要变的节点
                        Node<K,V> next;
                        do {     
                            next = e.next;
                            if ((e.hash & oldCap) == 0) { // 索引位置不用变
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {  // 索引位置要变
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;  // 新数组元素 指向 loHead
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;  //新数组元素 指向 hiHead
                        }
                    }
                }
            }
        }
        return newTab;
    }

扩容后,thresholdtable 的容量都变成原来的两倍。

当桶中只有一个节点时,新数组中的下标计算公式为 e.hash & (newCap - 1),原数组在新数组的索引下标一定为 i (原来的值),或者 i + oldCap

举个例子,假设某个元素的 hash 值为 3,扩容前数组的容量为 16,则扩容前该元素的索引下标为 3。

扩容后下标仍然为 3。

如果该元素的 hash 值为 19,则扩容前索引下标为 3。

扩容后索引下标为 3 + 16


当一个桶中的节点有多个时,并且是链表的形式,会把链表拆成两部分e.hash & oldCap = 0 时的节点,扩容后所在索引下标不变,否则索引下标为原来的索引加上 oldCap

最后执行 newTab[j] = loHeadnewTab[j + oldCap] = hiHead;,把扩容流程画一遍就很清楚了。


get()

查找比存进去简单多了。

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash &&  // 判断第一个节点是不是所找的节点
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) { 
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {  // 循环遍历
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;  // 不存在
    }

根据 key 算出节点所在的数组下标,然后遍历寻找。


安全问题

没有同步措施,可能会导致数据丢失,另外,在 jdk 1.7 中,扩容时可能发生死循环!!(可以搜一下为什么),jdk 1.8 时扩容不会发生死循环了。

HashTable 是线程安全的 hash 表,因为它的方法上加了 synchronized 同步锁,但是这样效率就很低,ConcurrentHashMap 应运而生。


ConcurrentHashMap

成员变量

    // 默认初始容量 
    private static final int DEFAULT_CAPACITY = 16;
    
    // 默认装填因子
    private static final float LOAD_FACTOR = 0.75f;
    
    // hash 表,大小一定是 2 的 n 次幂
    transient volatile Node<K,V>[] table;

    // 扩容时生成的新数组
    private transient volatile Node<K,V>[] nextTable;
    
    // -1 时代表初始化,-(1 + 正在扩容的线程数)表示正在扩容 ,默认为 0,通过 CAS 更新
    // 为正数时,表示阈值,即下一次扩容时的大小
    private transient volatile int sizeCtl;
    
    static final int MOVED     = -1; // forwarding 节点的 hash 值,用于扩容
    ...
    static final int HASH_BITS = 0x7fffffff; // 用于普通节点 hash
    
    static class Node<K,V> implements Map.Entry<K,V> {      // Node节点
        final int hash;
        final K key;
        volatile V val;  //  加了 volatile 修饰,保证了内存可见性
        volatile Node<K,V> next;
    }
    
    // UNSAFE机制
    ...

构造函数

    public ConcurrentHashMap() {
    }

    public ConcurrentHashMap(int initialCapacity) {
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

tableSizeFor() 返回大于等于 initialCapacity + (initialCapacity >>> 1) + 1 最小二次幂。比如:initialCapacity 为 7,返回的就是 16,ininitalCapacity 是 15,返回的就是 32。此时 table 没有初始化。


hash 函数

    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

也是混合原始哈希码的高位和低位,以此来加大低位的随机性,减少哈希碰撞


初始化函数

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)   // sizeCtl < 0 代表别的线程正在初始化
                Thread.yield(); // 让出 CPU 调度,直到初始化完成
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 如果 sizeCtl 的值为 0,则 CAS 修改为 -1
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        // 初始化容量为之前的 sizeCtl 或者 默认大小 16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY; 
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);  // 阈值为 table 大小的四分之三
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

我们来看看初始化函数是如何保证线程安全的

当一个线程发现 sizeCtl 已经被修改了,说明已经有线程正在初始化 table 了,当前线程则一直让出 CPU 调度,直到初始化完成。


put()

ConcurrentHashMap 通过 CAS + synchronized 保证了存入值时的线程安全性,我们来看看具体如何实现的。

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key 和 null 不允许为空
        if (key == null || value == null) throw new NullPointerException();
        // 计算 hash 值
        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,    // 直接 CAS 设置节点
                             new Node<K,V>(hash, key, value, null)))
                    break;                 
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f); // 帮助扩容
            else {
                V oldVal = null;
                synchronized (f) {  // 对桶中的头结点上锁!!!
                    if (tabAt(tab, i) == f) { 
                        if (fh >= 0) { // fh > 0,说明是普通链表节点
                            binCount = 1; // 记录桶中的元素个数
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;  
                                if (e.hash == hash &&  // 节点的 key 相同
                                    ((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) {
                            // 树节点,略过
                            bindCount = 2; // 树节点 bindCount 一直为 2
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);   // 链表转红黑树
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 更新
        addCount(1L, binCount);
        return null;
    }

如果 节点的 hash > 0,说明是普通链表节点,则遍历循环,在尾部添加新的节点。

当一个线程执行 put() 时,会对桶中的头结点进行加锁操作,保证了同一时间只有一个线程能操作该桶中的元素


扩容操作

多线程下是如何扩容的?如何保证线程安全?


addCount()

    private final void addCount(long x, int check) {
        ...
        // 获取 hash 表中节点的个数 
        s = sumCount();
        
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            
            // 判断表中的节点个数是否大于等于阈值
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                // 根据数组长度得到一个标识,具体的细节本文没有分析
                int rs = resizeStamp(n);
                if (sc < 0) { // sc < 0 说明正在扩容
                    // 扩容结束 或者 转移状态变化 或者 扩容线程达到最大数,退出循环
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    // 扩容还未结束,当前线程加入扩容  
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                // 此时还没有线程扩容,更新 sc,开始扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

sumCount() 能获取 hash 表中节点的个数HashMap 中是桶的个数),利用了 LongAdder 的思想,具体细节就不讨论了。

当节点的个数达到阈值时就扩容,不再与桶的个数相关


transfer()

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;  
        // 计算每条线程负责迁移的桶个数,每条线程最少处理 16 个桶
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; 
        if (nextTab == null) {
            try {
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 新表为原来的两倍大小
                nextTab = nt; 
            } catch (Throwable ex) {     
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n; // 给下一个线程分配任务区间的起始下标,从右往左分配的
        }
        int nextn = nextTab.length; // 新数组长度
        
        // 新建一个占位对象,hash 值为 MOVED
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        
        boolean advance = true; // 用于控制当前线程是否处理下一个桶
        boolean finishing = false;   // 表示扩容结束
        for (int i = 0, bound = 0;;) {  
            Node<K,V> f; int fh;
            while (advance) {
                int nextIndex, nextBound;
                // 代表当前线程的任务处理完毕,每次处理完一个桶,i--
                if (--i >= bound || finishing)
                    advance = false;
                // transferIndex <=0 说明所有的桶已被线程分配完毕
                else if ((nextIndex = transferIndex) <= 0) { // nextIndex = transferIndex
                    i = -1;
                    advance = false;
                }
                // 首次进入 for 循环时执行,分配当前线程的任务区间
                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 = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1); // 更新阈值为原来的两倍
                    return;
                }
                // 每条线程扩容结束后都会执行 sizeCtl - 1
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; 
                }
            }
            else if ((f = tabAt(tab, i)) == null)  // 当数组上的位置为空时,放置一个占位对象
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED) // f.hash == MOVED,防止别的线程在该桶上执行 put 操作
                advance = true; 
            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);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd); // 迁移完成后,将占位对象设置到该桶上
                            advance = true;
                        }
                        else if (f instanceof TreeBin) {
                            // 树节点
                        }
                    }
                }
            }
        }
    }

通过计算 CPU 的个数和哈希表的长度得到每个线程负责迁移的桶个数,每个线程最少处理 16 个桶,并且每个线程负责迁移的数量是平均的。

i 是线程负责开始迁移的下标,bound 是线程负责结束迁移的下标,迁移的顺序是从右往左!!!

线程分配转移任务的过程:假设 table 的长度为 64,stride 为 16。则 transferIndex 初始值为 64,假设有三个线程 A、B、C。

线程 A 执行扩容,则分配区间为 【64-16,64】=【48,64】,然后 transferIndex 的值更新为 48。

截止线程 B 执行扩容,则分配区间 【32,48】,transferIndex 的值更新为 32。

截止线程 C 执行扩容,分配区间 【16,32】,transferIndex 的值更新为 16。

最后一块区间【0,16】,线程 A、B、C 谁先执行完谁就去负责最后一块区间的转移。

转移链表时,和 HashMap思想几乎一样,根据 节点.hash & n 的值为 0 还是 为 1,分为两组,转移时会锁住桶的头节点,这里转移时用的是头插法,大家画下图就发现很简单。

ForwardingNode 占位节点的用处:防止别的线程在该桶上执行 put() 等修改操作。


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) // 树节点 或者 占位节点(表示当前 hash 表处于扩容状态)
                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() 函数没有加锁。当桶中头结点的 hash < 0,代表当前节点是树节点,或者当前 hash 表处于扩容中,则调用 TreeBinfind() 方法,或者调用转移节点的 find() 方法,或者被阻塞(这个桶中的节点刚好在转移,头结点被锁住了)。


总结

JDK 1.8 中采用了 Synchronized + CAS 的方式实现了线程安全,每次只锁住当前操作的桶的头结点,减少了锁粒度,提高了效率。

ConcurrentHashMap 中还有很多细节本文没有分析到,以后有时间再补充。