HashMap与ConcurrentHashMap万字源码分析

3 篇文章 0 订阅
1 篇文章 0 订阅

HashMapConcurrentHashMap`源码解析

JDK版本:1.7 & 1.8

​ 开发中常见的数据结构有三种:

1、数组结构:存储区间连续、内存占用严重、空间复杂度大

  • 优点:因为数组是连续的,所以随机读取和修改效率高,随机访问性强,查找速度快。
  • 缺点:因为数组是连续的,当需要往数组中某一个位置插入一个元素时,这个位置后的所有元素都需要往后移动,并且数组大小固定,不容易进行动态扩展。

2、链表接口:存储区间离散、内存占用宽松、空间复杂度小

  • 优点:插入、删除速度快,只需要修改指向下一个对象的指针即可。同时链表结构内存利用率高,没有固定大小,动态扩展灵活。
  • 缺点:查找效率低,每次查询元素都需要从第一个元素开始遍历查找。

3、哈希表结构:结合了数组结构与链表结构的优点,从而实现查找、修改、插入、删除效率都高的一种数据结构

​ 常见的HashMap的底层实现在JDK 8版本前后都有不同,在JDK 8之前HashMap底层采用数组+链表的数据结构实现,称之为Entry。在JDK 8之后HashMap底层采用数组+链表+红黑树实现称之为Node

1.JDK 7 HashMap

JDK 7HashMap的底层数据结构是数组链表。其具体结构如下图所示:

hashMapJDK7

​ 其中Entry的定义如下:

static class Entry<K, V> implements Map.Entry<K, V> {
    final K key;
    
    V value;
    
    Entry<K, V> next;
    
    int hash;
}
1.1put()过程

​ 相较于从HashMap中获取元素,可以先看HashMap是如何存储元素的。JDK 7HashMapput()方法,还是比较简单的:

public V put(K key, V value) {
    // 判断table是否为空, 如果为空则初始化数组大小
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 为 null, 最终会将这个entry放到table[0]中, 所以这也是为什么HashMap支持key为null
    if (key == null)
        return putForNullKey(value);
    // 1. 计算key的hash值, 返回一个32位的int整形
    int hash = hash(key);
    // 2. 根据key的hash值, 找到对应的数组下标。也就是找到当前key对应的value位于哪个桶中
    int i = indexFor(hash, table.length);
    // 3. 根据计算出的数组下标, 使用数组的随机访问获取对应数组桶上的链表, 遍历获取到的单向链表
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 链表元素的hash值与当前key计算出的hash值相等 && (链表元素中的key与当前key相等 || 链表元素中的key.equals(当前key))
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            // 进入该方法证明有此链表上存在重复的key
            // 获取当前链表中元素的value
            V oldValue = e.value;
            // 使用新value直接进行覆盖
            e.value = value;
            e.recordAccess(this);
            // 返回oldValue, put方法结束
            return oldValue;
        }
    }
	
    modCount++;
    // 4. 不存在重复的 key, 将此entry添加到链表中, 
    addEntry(hash, key, value, i);
    // 返回null
    return null;
}

​ 数组初始化inflateTable(threshold)方法,当往HashMapput()元素时,首先会检查此时数组是否为空,如果为空则会触发数组的初始化,在第一次往HashMapput()元素时会执行这个方法:

private void inflateTable(int toSize) {
    // 保证数组大小一定是2的n次方。它会计算出比threshold大的最小的2的次方数
    // 比如这样初始化:new HashMap(20),那么处理成初始数组大小是32
    int capacity = roundUpToPowerOf2(toSize);
    // 计算扩容阈值:capacity * loadFactor, 如果capacity * loadFactor大于HashMap的最大容量则默认使用MAXIMUM_CAPACITY + 1
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 初始化数组
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity); //ignore
}

​ 在执行数组初始化的时候,将数组大小保持为2n次方,这一点很重要,不论是JDK 7还是JDK 8中对HashMap的实现都是有相应的要求,因为后续会使用这个值做按位与操作。

​ 然后就是根据keyhash值与数组的长度计算出key的数组下标,indexFor(hash, table.length)方法:

static int indexFor(int hash, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return hash & (length-1);
}

​ 在初始化数组的方法中可以看到,数组的长度始终是2的次方。而使用key计算出的hash值是一个32位的整形。所以这里hash & (length-1)就是做按位与,比如此时数组长度为2n次方,那么此时计算出来的值就是hash值的低n位。

举个实际的例子来说:

​ 比如现在往new HashMap<String, String>(1<<4)put()一个值,比如这个值为kapcb

​ 根据上面分析的代码执行流程,首先会对这个HashMap的数组进行初始化。初始化完成之后由于这里创建HashMap的时候指定了初始容量的大小,所以数组的长度为16。然后计算出keyhash值,也就是kapcbhashCode,假设计算后得到的值为2759。那么hash & (length-1)这行代码做的事情就是这样的:

keyhash2759,对应二进制为:

	0000 0000 0000 0000 0000 1010 1100 0111

​ 数组长度为16(length-1)后得出的结果是15,对应二进制为:

	0000 0000 0000 0000 0000 0000 0000 1111

hash & (length-1)其实就是取低4位:

	0000 0000 0000 0000 0000 1010 1100 0111
&	
	0000 0000 0000 0000 0000 0000 0000 1111
 -------------------------------------------
	0000 0000 0000 0000 0000 0000 0000 0111

​ 此时hash & (length-1)计算出的结果就是0000 0000 0000 0000 0000 0000 0000 0111,也就是7。所以此时key对应的数组下标就是7。这也是为什么HashMap会要求数组初始容量必须是2n次方,因为当执行length - 1这步操作之后,对应二进制上的低位就全部是1,此时在做按位与的话,就能够保证取到的一定是keyhash值的低n位。

​ 所以,根据上面的分析也会得出,如果在创建HashMap实例的时候使用的是HashMap内部默认的capacity值。那么其实也就是取keyhash值的低5位。

​ 计算出key对应的数组下标之后,首先会获取数组对应下标上的单向链表,并对链表做遍历操作。如果在遍历链表的过程中,存在某一个节点的hash值和key与此时put()的元素的hash值与key相等,则直接对其进行覆盖并返回,此时put()函数就执行完成了。

​ 反之,就会将此时put()的元素添加到链表中:

void addEntry(int hash, K key, V value, int bucketIndex) {
    // 如果当前HashMap大小已经达到了阈值 && 新值要插入的数组位置已经有元素了, 则需要进行扩容
    // 这里的size记录的是HashMap中元素的总数, 也就是说当HashMap中元素的总数大于threshold时会触发扩容机制
    if ((size >= threshold) && (null != table[bucketIndex])) {
        // 扩容
        resize(2 * table.length);
        // 扩容以后, 重新计算hash值, 这里可以看到, HashMap针对key为null的情况是将其放在table[0]上的
        hash = (null != key) ? hash(key) : 0;
        // 重新计算扩容后的新的下标
        bucketIndex = indexFor(hash, table.length);
    }
    // 创建新节点并将使用头插法将元素添加到链表中
    createEntry(hash, key, value, bucketIndex);
}

// 这个很简单, 其实就是将新值放到链表的表头, 然后size++
void createEntry(int hash, K key, V value, int bucketIndex) {
    // 将数组中该位置上的元素赋值给e
    Entry<K,V> e = table[bucketIndex];
    // 将新元素放入数组中对应下标的位置, 并将Entry的next设置为e
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // 添加完成之后size++
    size++;
}

​ 在将新元素插入到链表中时,首先会判断此时HashMapsize是否超过了阈值,如果需要扩容则需要重新计算该数据在扩容后的table中的下标值,然后使用头插法将新元素添加到链表的头部。

1.2resize()数组扩容

​ 根据前面的源码分析,可以看到当往HashMap中添加新元素时,如果此时HashMapsize >= threshold或者数组对应下标上的值为null时,会触发HashMap的扩容机制,也就是resize(2 * table.length)这行代码:

void resize(int newCapacity) {
    // 获取当前的数组
    Entry[] oldTable = table;
    // 获取当前数组的容量
    int oldCapacity = oldTable.length;
    // 如果当前数组的容量 == HashMap默认的最大容量
    if (oldCapacity == MAXIMUM_CAPACITY) {
        // 那么将Integer.MAX_VALUE设置为HashMap的扩容阈值
        threshold = Integer.MAX_VALUE;
        // 直接返回
        return;
    }
    // 根据新的大小创建一个新的数组
    Entry[] newTable = new Entry[newCapacity];
    // 将原来数组中的值迁移到新的更大的数组中
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // 将新的table赋值给table进行替换
    table = newTable;
    // 重新设置HashMap的扩容阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

​ 扩容就是将创建一个容量翻倍的数组,将老数组上的数据迁移到新的数组上的过程。

​ 由于是双倍扩容,在迁移的过程中,会将原来table[i]中的链表的所有节点,分拆到新的数组的newTable[i]newTable[i+oldLength]位置上。

比如在扩容之前,HashMap的数组长度是16。那么在扩容之后,HashMap的数组长度会变为32。原来处于table[1]链表上的所有元素会被分配到新数组的table[1]table[17]这两个位置上。

1.3get()过程

​ 了解了HashMapput()过程,get()自然就容易理解得多。get()的过程大致分为以下三步:

  • 根据key计算出hash值。
  • 根据hash值计算出key对应的数组下标,hash & (length - 1)
  • 根据数组的随机访问机制,从数组中获取上一步计算出的数组下标上的链表。遍历链表,当存在== / equalskey时,返回结果。否则返回null
public V get(Object key) {
    // 首先判断key为null的话, 直接从table[0]获取链表进行遍历返回结果
    if (key == null)
        return getForNullKey();
    // 根据key获取数组中指定下标上的链表, 然后遍历找到符合条件的entry返回
    Entry<K,V> entry = getEntry(key);
	// 如果entry为null, 则代表HashMap中不存在该key返回null, 否则返回对应的value值
    return null == entry ? null : entry.getValue();
}

getEntry(key)方法源码:

final Entry<K,V> getEntry(Object key) {
    // 首先判断HashMap中是否有元素, 如果没有元素则直接返回null即可
    if (size == 0) {
        return null;
    }
	// 获取key的hash值
    int hash = (key == null) ? 0 : hash(key);
    // 确定数组下标, 然后从头开始遍历链表, 直到找到为止
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        // hash值相等 && (== || equals key) 判断链表中的元素节点的key是否与目标key相等
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            // 找到Entry直接返回
            return e;
    }
    // 没找到直接返回null
    return null;
}

​ 可以看到JDK 7HashMap的存取还是非常简单的,代码的思路也比较清晰。


2.JDK 7 ConcurrentHashMap

JDK 7中的ConcurrentHashMap其底层数据结果可以理解为数组和HashMap。其具体结构如下图所示:

concurrentHashMapJDK7

​ 整个ConcurrentHashMap是由一个个的Segment组成的,Segment代表部分一段的意思,所以很多地方都会将其描述为分段锁

​ 简单理解就是,ConcurrentHashMap是一个Segment类型的数组,Segment通过继承ReentrantLock来进行加锁,所以每次需要加锁的操作锁对象就是一个Segment,这样只要保证每个Segment是线程安全的,也就实现了容器的线程安全。

    /**
     * Stripped-down version of helper class used in previous version,
     * declared for the sake of serialization compatibility
     */
    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

concurrencyLevel:称为并行级别、并发数、Segment数。它的默认值是16,也就是说ConcurrentHashMap拥有16Segment。多以理论上,只要线程的操作分别在不同的Segment上,那么最多可同时支持16个线程并发写。这个值在ConcurrentHashMap初始化时可以进行设置,ConcurrentHashMap有提供此参数的构造器,但是一旦初始化之后,它的值是不可改变的,也就是说Segment是不可扩容的。

    /**
     * The default concurrency level for this table. Unused but
     * defined for compatibility with previous versions of this class.
     */
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

​ 每个Segment内部,其实与HashMap一样,不过它会保证线程安全。

​ 与HashMap一样,在ConcurrentHashMap内部同样有一些关键的变量:

  • initialCapacityConcurrentHashMap的初始容量,这个值是整个ConcurrentHashMap的初始容量,实际操作的时候需要平分给每个Segment
  • loadFactor:负载因子。因为当ConcurrentHashMap在初始化完成之后,Segment是不可进行扩容的,所以这个loadFactor是给每个Segment内部进行扩容使用的。

ConcurrentHashMap提供的配置concurrencyLevel的构造器:

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (concurrencyLevel > MAX_SEGMENTS)
        concurrencyLevel = MAX_SEGMENTS;
    
    int sshift = 0;
    int ssize = 1;
    // 计算并行级别ssize,因为要保持并行级别是2的n次方
    while (ssize < concurrencyLevel) {
        ++sshift;
        ssize <<= 1;
    }
    
    // 默认值concurrencyLevel为16, sshift为4
    // 那么计算出segmentShift为28, segmentMask为15, 后面会用到这两个值
    this.segmentShift = 32 - sshift;
    this.segmentMask = ssize - 1;

    // 为initialCapacity设置最大默认值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;

    // initialCapacity是设置整个map初始的大小,
    // 这里根据initialCapacity计算每个Segment数组中每个位置可以分到的大小
    // 如initialCapacity为64, 那么每个Segment或称之为"槽"可以分到4个
    int c = initialCapacity / ssize;
    if (c * ssize < initialCapacity)
        ++c;
    // 默认MIN_SEGMENT_TABLE_CAPACITY是2,这个值也是有讲究的,因为这样的话,对于具体的槽上,
    // 插入一个元素不至于扩容,插入第二个的时候才会扩容
    int cap = MIN_SEGMENT_TABLE_CAPACITY; 
    while (cap < c)
        cap <<= 1;

    // 创建Segment数组,
    // 并创建数组的第一个元素segment[0]
    Segment<K,V> s0 =
        new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                         (HashEntry<K,V>[])new HashEntry[cap]);
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
    // 往数组写入 segment[0]
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
    this.segments = ss;
}

​ 初始化完成之后,就会得到一个Segment数组。当使用new ConcurrentHashMap<String, Object>()无参构造器进行初始化时,那么初始化完成之后,ConcurrentHashMap内部的参数分为如下:

  • Segment数组长度为16,不可以扩容。
  • Sgement[i]的默认大小为2,负载因子是0.75,计算可以得出扩容阈值为2 * 0.75 = 1.5,也就是当往Segment中插入第二个元素时,Segment内部会进行一次扩容。
  • 这里已经初始化了segment[0],其它位置的值还是null
  • 当前的segmentShift的值为32 -4 = 28segmentMask的值为16 - 1 = 15
2.1put()过程
public V put(K key, V value) {
    Segment<K,V> s;
    // value为空, 直接抛出异常
    if (value == null)
        throw new NullPointerException();
    // 1. 计算key的hash值
    int hash = hash(key);
    // 2. 根据hash值找到Segment数组中的位置j
    //    hash是32位,无符号右移segmentShift(28)位,剩下高4位,
    //    然后和segmentMask(15)做一次按位与操作,也就是说j是hash值的高4位,也就是槽的数组下标
    int j = (hash >>> segmentShift) & segmentMask;
    // 刚刚说了,初始化的时候初始化了segment[0],但是其他位置还是null,
    // ensureSegment(j)对segment[j]进行初始化
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
         (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    // 3. 插入新值到槽s中
    return s.put(key, hash, value, false);
}

Segment内部是由数组+链表组成的。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // 在往该 segment 写入前,需要先获取该 segment 的独占锁
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        // 这个是 segment 内部的数组
        HashEntry<K,V>[] tab = table;
        // 再利用 hash 值,求应该放置的数组下标
        int index = (tab.length - 1) & hash;
        // first 是数组该位置处的链表的表头
        HashEntry<K,V> first = entryAt(tab, index);

        // 下面这串 for 循环虽然很长,不过也很好理解,想想该位置没有任何元素和已经存在一个链表这两种情况
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                // 如果链表上已经存在当前key
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        // 覆盖旧值
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                // 继续顺着链表走
                e = e.next;
            }
            else {
                // node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。
                // 如果不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头。
                if (node != null)
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);

                int c = count + 1;
                // 如果超过了该 segment 的阈值,这个 segment 需要扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    // 扩容
                    rehash(node); 
                else
                    // 没有达到阈值,将 node 放到数组 tab 的 index 位置,
                    // 其实就是将新的节点设置成原链表的表头
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        // 解锁
        unlock();
    }
    return oldValue;
}

ConcurrentHashMap整体的put()流程和思路还是非常清晰的,由于有独占锁保护,所以Segment内部的操作并没有过于复杂。

​ 至此put()操作就结束了。来看看关键的步骤。

​ 在ConcurrnetHashMap初始化的时候默认会初始化第一个槽segement[0],对于其它槽来说,在插入第一个值的时候进行segment的初始化。因为考虑到并发访问的问题,可能会存在多个线程同时进来初始化同一个槽segment[u],但是只需要一个成功即可:

private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    // 计算需要初始化的segement数组下标
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    // 如果此时segement数组的u下标为空
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        // 这里看到为什么之前要初始化 segment[0] 了,
        // 使用当前segment[0]处的数组长度和负载因子来初始化segment[k]
        // 为什么要用“当前”,因为segment[0]可能早就扩容过了
        Segment<K,V> proto = ss[0];
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);

        // 初始化 segment[k] 内部的数组
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
        // 再次检查一遍该槽是否被其他线程初始化了。
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) {
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // 使用 while 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出
            // 如果有一个线程成功初始化之后, 另外其它的线程就会CAS失败, 此时while循环的作用
            // 就体现出来了, 它会将seg赋值返回, 所以当一个线程成功初始化之后, 其它线程CAS失败同时
            // 会获取到seg
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

ensureSegment(int k)的流程与逻辑还是比较清晰的,使用自旋+CASsegment[u]执行初始化操作,保证只会有一个线程去初始化segment[u]

​ 在往segmentput()元素的时候,首先会调用HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);。也就是先使用tryLock()快速获取该segment的独占锁,如果失败,那么进入到scanAndLockForPut(key, hash, value)这个分支来获取锁。

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node

    // 循环获取锁
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                    // 进到这里说明数组该位置的链表是空的,没有任何元素
                    // 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                // 顺着链表往下走
                e = e.next;
        }
        // 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁
        //    lock() 是阻塞方法,直到获取锁后返回
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
                 // 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头
                 //     所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; // re-traverse if entry changed
            retries = -1;
        }
    }
    return node;
}

scanAndLockForPut(K key, int hash, V value)方法有两个出口,一个是tryLock()成功,循环终止。另一个就是重试次数超过了MAX_SCAN_RETRIES,那么就会调用lock()方法阻塞等待,直至成功拿到独占锁。

​ 这个方法看似复杂,其实只做了一件事,就是**获取该segment**的独占锁,如果需要的话顺便实例化了一下node

​ 在put()元素到COncurrentHashMap中时,首先会将当前ConcurrentHashMap中的数据量累加1,然后判断是否大于扩容阈值,并且此时的阈值是小于ConcurrentHashMap的最大容量的,也就是判断该值的插入是否会导致该segment的元素个数超过阈值。如果大于则会触发当前segment槽的扩容机制。

put()方法中涉及扩容的代码片段:

                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    // 扩容
                    rehash(node); 

​ 首先是c值得累加,以及该方法的判断都不会有并发安全问题,因为此时以及获取到了该Segment的独占锁。只会有当前线程才能进入互斥区。

rehash(node)

// 方法参数上的 node 是这次扩容后,需要添加到新的数组中的数据。
private void rehash(HashEntry<K,V> node) {
    HashEntry<K,V>[] oldTable = table;
    int oldCapacity = oldTable.length;
    // 左移一位相当于扩容为之前容量的2倍
    int newCapacity = oldCapacity << 1;
    // 计算新的扩容阈值
    threshold = (int)(newCapacity * loadFactor);
    // 创建新数组
    HashEntry<K,V>[] newTable =
        (HashEntry<K,V>[]) new HashEntry[newCapacity];
    // 新的掩码,如从 16 扩容到 32,那么 sizeMask 为 31,对应二进制 ‘000...00011111’
    int sizeMask = newCapacity - 1;

    // 遍历原数组,老套路,将原数组位置 i 处的链表拆分到 新数组位置 i 和 i+oldCap 两个位置
    for (int i = 0; i < oldCapacity ; i++) {
        // e 是链表的第一个元素
        HashEntry<K,V> e = oldTable[i];
        if (e != null) {
            HashEntry<K,V> next = e.next;
            // 计算应该放置在新数组中的位置,
            // 假设原数组长度为 16,e 在 oldTable[3] 处,那么 idx 只可能是 3 或者是 3 + 16 = 19
            int idx = e.hash & sizeMask;
            if (next == null)   // 该位置处只有一个元素,那比较好办
                newTable[idx] = e;
            else { // Reuse consecutive sequence at same slot
                // e 是链表表头
                HashEntry<K,V> lastRun = e;
                // idx 是当前链表的头结点 e 的新位置
                int lastIdx = idx;

                // 下面这个 for 循环会找到一个 lastRun 节点,这个节点之后的所有元素是将要放到一起的
                for (HashEntry<K,V> last = next;
                     last != null;
                     last = last.next) {
                    int k = last.hash & sizeMask;
                    if (k != lastIdx) {
                        lastIdx = k;
                        lastRun = last;
                    }
                }
                // 将 lastRun 及其之后的所有节点组成的这个链表放到 lastIdx 这个位置
                newTable[lastIdx] = lastRun;
                // 下面的操作是处理 lastRun 之前的节点,
                //    这些节点可能分配在另一个链表中,也可能分配到上面的那个链表中
                for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
                    V v = p.value;
                    int h = p.hash;
                    int k = h & sizeMask;
                    HashEntry<K,V> n = newTable[k];
                    newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
                }
            }
        }
    }
    // 将新来的 node 放到新数组中刚刚的 两个链表之一 的 头部
    int nodeIndex = node.hash & sizeMask; // add the new node
    node.setNext(newTable[nodeIndex]);
    newTable[nodeIndex] = node;
    table = newTable;
}

​ 需要注意的是,一旦ConcurrentHashMap初始化完成之后,segment数组是不能进行扩容的,扩容的是segment数组上某个segment元素内部的HashEntry<K, V>数组,同HashMap一样,扩容之后的容量为之前的2倍。

​ 可以看到最后是连着两个for循环,如果找到的lastRun节点在链表中属于靠前的位置,那么这样做一次遍历找到这个节点还是非常有意义的,因为这样的话只需要克隆lastRun前面的节点,后面的一串节点跟着lastRun走就行了,不需要做任何操作。

Doug Lea的这个思想还是非常有意思的,但是如果出现比较坏的情况就是lastRun都是链表靠后或者最后一个元素时,那么前面的这次for循环遍历链表就显的优点浪费性能了。不过Doug Lea也说了,根据数据统计,如果使用默认的阈值,大约只有1 / 6的节点需要克隆。

2.2get()过程

​ 相对于put()来说,这里的get()就非常简单了。

  • 计算keyhash值,找到segment数组中对应的segment
  • segment中也是一个HashEntry<K, V>数组,再根据hash值和当前数组的长度减1找到HashEntry<K, V>的位置。
  • 遍历链表,依次查找。找到则返回value,没找到则返回null
public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    // 1. 获取key的hash值
    int h = hash(key);
    // 计算key位于segment数组中的哪个segment上
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    // 2. 根据u找到对应的segment
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        // 3. 找到segment内部数组相应位置的链表遍历
        for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
                 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
             e != null; e = e.next) {
            K k;
            if ((k = e.key) == key || (e.hash == h && key.equals(k)))
                return e.value;
        }
    }
    return null;
}

​ 可以看到在get()过程中是没有加锁的,所以在执行get()操作是存在并发安全问题的。

ConcurrentHashMap中添加节点的操作put()和删除节点的操作remove()都是会在当前segment上加独占锁的,所以当对于同一个segment上的元素进行put() / remove()操作时,是线程安全的。但是获取元素的操作get()是没有在segment上加独占锁的。所以在并发情况下,对于同一个segment上的元素,当一个线程在执行get()操作时,其它线程执行了put() / remove()操作,此时就可能存在并发问题。

put()操作的线程安全性:

  • 初始化segment时,使用的是CAS操作来初始化segment
  • 添加节点到链表中的操作是头插法,所以,如果此时get()操作在遍历链表的过程中已经到了链表的中间,是没有任何影响的。当在put()操作之后立即执行get(),则需要保证新插入链表头部的元素对其它线程可见,这个依赖于setEntryAt()方法中使用的UNSAFE.putOrderedObject
  • segment内部的数组执行扩容时。首先扩容的步骤是先创建一个扩容后的新HashEntry<K, V>数组,然后将数据从oldTable迁移到newTable中,然后将newTable赋值给table成员变量。需要注意的是,只有在执行put()操作的时候,才有可能会进行扩容操作。如果put()先行,那么put()操作的可见性就是table成员变量使用volatile关键字修饰的。如果get()先行,此时的get()还是在旧的table上做查询操作,对结果是没有影响的。

remove()操作的线程安全性:

  • 因为get()操作需要遍历链表,但是remove()操作会破坏链表。
  • 如果remove()破坏的节点get()操作已经遍历过去了,那么此时是不存在问题的。
  • 如果remove()破坏了一个节点,需要分两种情况考虑。
    • 如果此节点是头节点,那么需要将头节点的next设置为数组当前位置的元素,即将链表往前推一下。虽然table成员变量是使用volatile关键字修饰,但是volatile关键字并不能提供数组内部操作的可见性,所以在JDK 7的源码中使用了UNSAFE来操作数组,具体实现逻辑在setEntryAt
    • 如果此节点不是头节点,当删除链表中的某一个节点之后,它需要将删除节点的前置节点的next指向删除节点的后置节点,这里的并发保证就是next属性使用volatile关键字保证内存可见性。

3.JDK 8 HashMap

JDK 8HashMap的底层数据结构是数组链表红黑树

​ 根据JDK 7HashMap的源码分析,针对JDK 7HashMapget()操作。首先会根据key的值计算出数组的具体下标,找到对应的数组元素之后,需要从前到后依次遍历链表,寻找目标节点。这种情况下,get()操作的时间复杂度取决于当前链表的长度,所以时间复杂度为O(n)

​ 为了降低这部分开销,在JDK 8中,当链表中的元素达到8时,会将链表转换为红黑树,当转换为红黑树之后,针对这些位置的查找,时间复杂度可以降低到O(logn)

JDK 8HashMap的具体结构如下图所示:

hashMapJDK8

>注意,此图仅为`JDK 8`中的`HashMap`的大致结构示意图。

​ 在JDK 7中使用的是Entry来代表每个HashMap中的数据节点,JDK 8中使用Node,基本没有区别。其内部都是keyvaluehashnext四个属性。但是在JDK 8Node只能用于链表的情况,当链表转换为红黑树时使用的是TreeNode。这一点在源码中也会有体现,会对数组上的第一个节点数据类型做判断,如果是Node则代表当前位置下是链表,如果是TreeNode则代表当前节点下是红黑树。

3.1put()过程
    /**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

先翻译注释:

​ 将指定的键值对放入当前的map中。如果当前map中包含映射的键,则替换掉旧值。

​ 调用put()方法首先会执行hash(key)hash(Object key)的源码如下:

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        // 如果key为null, 直接返回0, 否则将key的hashCode的高16位与低16位做异或返回
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

先翻译一下注释:

​ 计算key.hashCode()并将哈希的较高位传播(XOR)到较低位。 由于该表使用二次幂掩码,因此仅在当前掩码之上位变化的散列集将始终发生冲突。(已知的例子是在小表中保存连续整数的Float键集)。因此,我们应用了一种变换,将高位的影响向下传播。在位扩展的速度、实用性和质量之间存在折衷。因为许多常见的散列集已经合理分布(所以不要从传播中受益),并且因为我们使用树来处理bin中的大量冲突,我们只是以最便宜的方式对一些移位的位进行异或,以减少系统损失,以及合并最高位的影响,否则由于表边界,这些最高位将永远不会用于索引计算。

思考一下,为什么要将keyhashCode的高16位与低16位做异或?

来看看32位整型的高16位与低16位做异或具体是怎样的一个过程:

​ 假设当前有一个key,调用其hashCode()方法返回的32位整型值为1124758,那么其二进制形式为:

	0000 0000 0001 0001 0010 1001 1001 0110

​ 当执行无符号位右移h>>>16之后得到的结果为:

	0000 0000 0000 0000 0000 0000 0001 0001

​ 然后执行异或(h = key.hashCode()) ^ (h >>> 16)之后的结果为:

	0000 0000 0001 0001 0010 1001 1001 0110
^
	0000 0000 0000 0000 0000 0000 0001 0001
-------------------------------------------
 	0000 0000 0001 0001 0010 1001 1000 0111

​ 即结果为1124743

​ 在二进制中,左移n位,相当于乘以2n次方。右移n位,相当于除以2n次方。

思考一下,这里为什么要用异或,而不用与或非?

​ 这就涉及到概率学的问题了,hash(Object key)是散列函数,如果散列得越开,那么后期发生hash碰撞的概率就越低。

​ 对于&来说,二进制位上25%的概率为175%的概率为0

​ 对于|来说,二进制位上75%的概率为125%的概率为0

​ 对于^来说,二进制位上50%的概率为150%的概率为0

​ 所以可以看出,为了尽可能做到均匀散列,采用概率对等的^

​ 当keyhash散列值计算出来之后,就会进入HashMap真正的存储数据的方法putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key	第一个参数: key的hash值
     * @param key the key 第二个参数:key
     * @param value the value to put 第三个参数:value
     * @param onlyIfAbsent if true, don't change existing value 第四个参数:当onlyIfAbsent为true时, 在HashMap中存在当前key时不会执行put操作, 即不会进行覆盖
     * @param evict if false, the table is in creation mode. 第五个参数:不需要关注
     * @return previous value, or null if none 返回值:返回之前key对应的value, 如果不存在key则返回null
     */ 
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // 第一次执行put操作时, 会判断数组是否为null或者数组长度为0,  如果是则触发数组的扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            // 第一次resize()和后续的扩容有些不一样, 因为这次是数组从null初始化到默认值16或自定义的初始容量
            n = (tab = resize()).length;
        // 将(tab.length - 1) & hash做按位与得到数组下标, 然后获取到数组中的当前下标的值, 如果值为null, 则初始化Node并将其放在数组的当前位置上
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 初始化Node, 并将初始化的Node放在数组的当前下标位置上
            tab[i] = newNode(hash, key, value, null);
        // 如果数组该下标的位置上有数据
        else {
            Node<K,V> e; K k;
            // 首先判断数组该下标上的数据与当前需要插入的数据的hash以及key相等, 如果相等, 则取出当前数据
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 取出当前数据
                e = p;
            // 如果数组该下标上的数据类型为TreeNode, 则代表下面是颗红黑树
            else if (p instanceof TreeNode)
                // 如果是红黑树则调用红黑树的putTreeVal()方法插入值
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		   // 否则数组改下表上的数据类型为Node, 则代表下面是一个链表	            
            else {
                // 对链表进行遍历
                for (int binCount = 0; ; ++binCount) {
                    // 获取下一个节点, 同时判断下一个节点是否为空
                    if ((e = p.next) == null) {
                        // 如果为空则创建一个新的Node, 并将新的Node放到链表的尾部
                        p.next = newNode(hash, key, value, null);
                        // 当插入当前数据后链表长度大于8
                        // 注意这里的减1是因为是从索引0开始的
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 将链表转换为红黑树
                            treeifyBin(tab, hash);
                        // 退出循环
                        break;
                    }
                    // 如果在链表中找到了与key相等的节点数据
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 退出循环
                        break;
                    p = e;
                }
            }
            // 如果 e != null, 说明此时HashMap中存在旧的值的key与当前要插入值得key相等
            if (e != null) { // existing mapping for key
                // 取出老的value
                V oldValue = e.value;
                // onlyIfAbsent为false || 老的value为null
                if (!onlyIfAbsent || oldValue == null)
                    // 覆盖老的value
                    e.value = value;
                afterNodeAccess(e);
                // 返回旧值
                return oldValue;
            }
        }
        ++modCount;
        // 如果插入值之后, HashMap中的元素个数达到了阈值
        if (++size > threshold)
            // 触发扩容
            resize();
        afterNodeInsertion(evict);
        return null;
    }

JDK 7会在数据插入之前进行判断是否需要对数组进行扩容,JDK 8是将值插入之后再判断是否需要对数组进行扩容。

​ 不论是JDK 7还是JDK 8HashMap的扩容都是非常重要的。在JDK 7HashMap数组的初始化和扩容是两个方法。而在JDK 8中数组的初始化和扩容是同一个方法。并且都是扩容为之前容量的2倍,然后将数据从oldTable迁移到newTable

   /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        // 获取当前table数组
        Node<K,V>[] oldTab = table;
        // 获取当前table数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 获取当前的扩容阈值
        int oldThr = threshold;
        // newCap是扩容之后新table的大小, newThr是扩容之后新的扩容阈值
        int newCap, newThr = 0;
        // 如果当前table数组长度大于0, 说明HashMap已经正常初始化过了, 是一次正常的扩容操作
        if (oldCap > 0) {
            // 判断旧的容量是否大于等于HashMap的最大容量, 如果是则无法进行扩容, 并且设置扩容条件为Integer最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                // 修改当前HashMap的扩容阈值为Integer.MAX_VALUE
                threshold = Integer.MAX_VALUE;
                // 无法进行扩容, 直接返回当前table
                return oldTab;
            }
            // 将oldCap左移一位然后赋值给newCap, 即扩容为当前table的2倍
            // 如果扩之后的table长度小于HashMap的最大默认长度 && oldCap >= 16 
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 将扩容阈值扩大一倍
                newThr = oldThr << 1; // double threshold
        }
        // 如果oldThr=0并且边界值大于0, 说明散列表是null
        // 1.应对使用new HashMap<K, V>(int initialCapacity, float loadFactor)初始化后, 第一次调用put进行扩容
        // 2.应对new HashMap<K, V>(int initialCapacity)初始化后, 第一次调用put进行扩容
        // 3.应对new HashMap<K, V>(Map<? extends K, ? extends V> m)初始化后, 第一次调用put进行扩容
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 这种情况下oldThr=0;oldCap=0;说明没有经过初始化, 需要初始化HashMap
        // 应对使用new HashMap<K, V>()初始化后, 第一次调用put进行扩容
        else {               // zero initial threshold signifies using defaults
            // newCap = DEFAULT_INITIAL_CAPACITY = 16;
            newCap = DEFAULT_INITIAL_CAPACITY;
            // 根据默认的初始容量计算扩容阈值
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        // newThr为0时, 通过newCap和loadFactor计算出一个newThr
        if (newThr == 0) {
            // newCap * 0.75
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        // 用新的数组大小初始化新的数组
        @SuppressWarnings({"rawtypes","unchecked"})
        // 如果是第一次调用put存储元素, 那么此时数组初始化完成, 执行到这里就结束了, 直接返回newTab即可
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        // 将table指向newTab
        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);
                    // 如果下面不止一个节点, 并且节点数据类型不是TreeNode, 则此时是链表
                    else { // preserve order
                        // 将链表拆成两个链表, 然后放到新数组中, 并且保留当前节点的先后顺序
                        // loHead和loTail对应一条链表, 称为低位链表lo
                        // hiHead和hiTail对应一条链表, 称为高位链表hi
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 遍历链表中的所有节点
                        do {
                            next = e.next;
                            // oldCap一般都是2的n次方, 使用节点的hash与2的n次方做按位与, 可以得到高位为1还是0
                            // 如果高位为0则将其放在低位链表中
                            if ((e.hash & oldCap) == 0) {
                                // 如果链表尾节点为空
                                if (loTail == null)
                                    // 将当前节点设为链表的头节点
                                    loHead = e;
                                // 如果链表不为空
                                else
                                    // 将当前节点设为链表的尾节点
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 高位为1进入高位链表
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        // 低位链表已成, 将低位链表的loHead指向扩容后的原位
                        if (loTail != null) {
                            // 将尾节点的next置为null, 防止链表在切分时是之前链表的中间节点
                            loTail.next = null;
                            // 将lo链表放到扩容后数组的j位置, 也就是与扩容之前相同的位置
                            newTab[j] = loHead;
                        }
                        // 高位链表已成, 将高位链表的hiHead指向扩容后的新索引地址
                        if (hiTail != null) {
                            // 将尾节点的next置为null, 防止链表在切分时是之前链表的中间节点
                            hiTail.next = null;
                            // 将hi链表放到扩容后数组的j + oldCap位置
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

resize()方法中的代码逻辑和思路还是非常清晰的。

3.2get()过程

​ 相对于put()方法来说,get()方法就非常简单了。

  • 计算keyhash值,根据hash & (table.length - 1)的值找到对应的数组下标。
  • 判断数组该位置上的元素是否刚好就是目标元素,如果不是,则继续判断是红黑树还是链表。
  • 如果该元素的数据类型是TreeNode,则使用红黑树的方式get()数据。
  • 如果该元素的数据类型不是TreeNode,则遍历链表,直到找到与key相等的节点。
    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

​ 这里还是会优先执行hash(key)

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

​ 然后调用getNode(int hash, Object key)获取目标元素:

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 主要是判断该数组位置上的元素是否为空, 如果为空代表此时HashMap中不存在该key的值, 直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 判断第一个节点是不是目标元素, 如果是直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 如果不是, 并且数组当前位置上存在不止一个元素, 如果只存在一个元素, 并且不是目标元素, 那么HashMap中也不存在当前key对应的value
            if ((e = first.next) != null) {
                // 判断是否是红黑树
                if (first instanceof TreeNode)
                    // 使用红黑树的方式查找元素
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 如果是链表, 则遍历链表
                do {
                    // 如果在链表中找到与key相等的节点
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 返回该节点
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

​ 可以看到get()也是非常简单的。


4.JDK 8 ConcurrentHashMap

JDK 8中的ConcurrentHashMap也引入了红黑树。在结构上JDK 8ConcurrentHashMapHashMap的底层结构基本上是一样的,不过ConcurrentHashMap要保证线程安全。

JDK 8ConcurrentHashMap的具体结构如下图所示:

concurrentHashMapJDK8

注意,此图仅为JDK 8中的ConcurrentHashMap的大致结构示意图。其结构与JDK 8HashMap基本上一样。

JDK 8中的ConcurrentHashMap中提供了5个构造器。

    /**
     * Creates a new, empty map with the default initial table size (16).
     */
    public ConcurrentHashMap() {
    }

    /**
     * Creates a new, empty map with an initial table size
     * accommodating the specified number of elements without the need
     * to dynamically resize.
     *
     * @param initialCapacity The implementation performs internal
     * sizing to accommodate this many elements.
     * @throws IllegalArgumentException if the initial capacity of
     * elements is negative
     */
    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;
    }

    /**
     * Creates a new map with the same mappings as the given map.
     *
     * @param m the map
     */
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }

    /**
     * Creates a new, empty map with an initial table size based on
     * the given number of elements ({@code initialCapacity}) and
     * initial table density ({@code loadFactor}).
     *
     * @param initialCapacity the initial capacity. The implementation
     * performs internal sizing to accommodate this many elements,
     * given the specified load factor.
     * @param loadFactor the load factor (table density) for
     * establishing the initial table size
     * @throws IllegalArgumentException if the initial capacity of
     * elements is negative or the load factor is nonpositive
     *
     * @since 1.6
     */
    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

    /**
     * Creates a new, empty map with an initial table size based on
     * the given number of elements ({@code initialCapacity}), table
     * density ({@code loadFactor}), and number of concurrently
     * updating threads ({@code concurrencyLevel}).
     *
     * @param initialCapacity the initial capacity. The implementation
     * performs internal sizing to accommodate this many elements,
     * given the specified load factor.
     * @param loadFactor the load factor (table density) for
     * establishing the initial table size
     * @param concurrencyLevel the estimated number of concurrently
     * updating threads. The implementation may use this value as
     * a sizing hint.
     * @throws IllegalArgumentException if the initial capacity is
     * negative or the load factor or concurrencyLevel are
     * nonpositive
     */
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

​ 其中第二个有参构造器是特别有意思的:

    public ConcurrentHashMap(int initialCapacity) {
        // 如果指定ConcurrentHashMap的初始容量小于0
        if (initialCapacity < 0)
            // 抛出异常
            throw new IllegalArgumentException();
        // 如果指定的initialCapacity小于ConcurrentHashMap的默认最大容量, 则调用tableSizeFor
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

​ 当调用这个构造器初始化一个ConcurrentHashMap时,需要指定HashMap的初始容量。通过提供的初始容量initialCapacity,计算出了sizeCtl

​ 这里sizeCtl的具体计算方式为tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1)。也就是[(initialCapacity * 1.5 + 1), 向上取最近的2的n次方]

例如initialCapacity10,那么sizeCtl就为16

4.2put()过程
    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

​ 这里直接调用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();
        // 计算key的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();
            // 如果数组不为空, 根据hash值与数组长度减一做按位与, 获取数组指定下标的元素
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 如果数组该位置为空, 则使用一次CAS操作, 如果CAS成功将值设置数组的当前位置, 那么直接break, put方法结束
                // 如果CAS失败, 那么证明存在并发, 已经有其它线程抢先将值放上去了, 此时进入下一个循环, 会进入到存在值的分支进行处理
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果数组当前位置上的元素不为空, 则判断第一个元素的hash是否等于MOVED
            else if ((fh = f.hash) == MOVED)
                // 帮助数据迁移
                tab = helpTransfer(tab, f);
            // f是该位置的头节点, 并且f不为空
            else {
                V oldVal = null;
                // 获取数组该位置头节点的对象锁
                synchronized (f) {
                    // 获取头节点并判断此时头节点是否还是f
                    if (tabAt(tab, i) == f) {
                        // 头节点的hash值大于0, 说明是链表
                        if (fh >= 0) {
                            // 用于累加, 记录链表的长度
                            binCount = 1;
                            // 遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 如果在链表中找到了与key相等的元素
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    // 判断是否需要对oldValue进行覆盖
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    // 退出, put执行完成
                                    break;
                                }
                                Node<K,V> pred = e;
                                // 如果到了链表的尾部
                                if ((e = e.next) == null) {
                                    // 则将新值放到链表尾部
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    // 退出, put执行完成
                                    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) {
                    // 插入值完成之后, 判断链表中的元素个数是否大于8
                    if (binCount >= TREEIFY_THRESHOLD)
                        // 如果大于8则将链表转换为红黑树
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

​ 这里有几个关键的点,第一个是数组的初始化,第二个是数组的扩容,第三个是帮助数据迁移。

​ 第一次put()值是会触发数组的初始化,initTable()

    /**
     * Initializes table, using the size recorded in sizeCtl.
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            // 初始化的执行被其它线程抢占了
            if ((sc = sizeCtl) < 0)
                // 调用yield()方法主动退出CPU时间片
                Thread.yield(); // lost initialization race; just spin
            // 使用CAS将sizeCtl的值由sc设置为-1, 代表争抢到了锁
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        // DEFAULT_CAPACITY 默认初始容量是 16
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        // 初始化数组, 将数组长度设置为ConcurrentHashMap初始化时指定的值或16
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        // 这里其实就是sc = 0.75 * n
                        sc = n - (n >>> 2);
                    }
                } finally {
                    // 重新设置sizeCtl的值
                    sizeCtl = sc;
                }
                // 退出
                break;
            }
        }
        // 返回初始化完成的数组
        return tab;
    }

​ 初始化数组大小的方法,主要就是初始化为一个合适的大小的数组,然后设置sizeCtl的值。在初始化过程中的并发问题是通过对sizeCtl进行CAS操作来实现的。

​ 将链表转换为红黑树,但是将链表转换为红黑树也并不是绝对的,有可能也是对数组做扩容。treeifyBin(tab, i)源码:

    /**
     * Replaces all linked nodes in bin at given index unless table is
     * too small, in which case resizes instead.
     */
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            // 如果数组长度小于64, 其实也就是32、16、8或者更小的时候, 则只会进行数组的扩容
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            // b是头节点
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                // 加锁
                synchronized (b) {
                    // 双重验证
                    if (tabAt(tab, index) == b) {
                        // 遍历链表, 将链表数据转移到红黑树上
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            // 创建一个TreeNode
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        // 将红黑树设置到数组的相应位置
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

​ 当ConcurrentHashMap中链表长度大于8时,会调用treeifyBin(Node<K,V>[] tab, int index)将链表转换为红黑树。但是当进入到该方法之后,它首先会判断当前数组的大小是否大于等于64,如果此时数组的长度大于等于64,那么它才会将链表转换为红黑树。如果此时数组的长度小于64,那么此时会调用tryPresize(n << 1)对数组进行扩容。

    /**
     * Tries to presize table to accommodate the given number of elements.
     *
     * @param size number of elements (doesn't need to be perfectly accurate)
     *
     * 此时的size已经是当前数组长度扩容之后的大小
     * 
     */
    private final void tryPresize(int size) {
        // c:扩容后的大小无符号位右移1位, 然后加上size, 其实就是(size * 1.5) + 1然后向上取最近的2的n次方
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            // 如果数组此时还未初始化, 则初始化数组并设置sizeCtl
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            }
            else if (c <= sc || n >= MAXIMUM_CAPACITY)
                break;
            else if (tab == table) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    Node<K,V>[] nt;
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    // 2. 使用CAS将sizeCtl的值加1
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        // 如果CAS成功, 然后执行transfer()方法, 此时nextTable不为null
                        transfer(tab, nt);
                }
                // 1. 使用CAS将sizeCtl的值设置为(rs << RESIZE_STAMP_SHIFT) + 2)
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    // 如果CAS成功, 然后执行transfer()方法, 此时nextTable为null
                    transfer(tab, null);
            }
        }
    }

​ 这个方法的核心在于sizeCtl值得操作。首先使用CAS操作将sizeCtl设置为一个负数(rs << RESIZE_STAMP_SHIFT) + 2)CAS成功则执行transfer(tab, null)。到下一个循环中使用CASsizeCtl设置为sizeCtl + 1CAS成功则执行transfer(tab, nt)

​ 所以这里可能的操作就是执行1transfer(tab, null)和多次transfer(tab, nt)

​ 数组扩容后的数据迁移,调用transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)

    /**
     * Moves and/or copies the nodes in each bin to new table. See
     * above for explanation.
     */
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        
        // stride在单核CPU下直接等于n。 多核模式下为(n >>> 3) / NCPU, 最小值为16
        // stride可以理解为步长, 有n个位置是需要进行迁移的
        // 将这n个任务分为多个任务包, 每个任务包有stride个任务
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        // 如果nextTab为null, 则先对其进行初始化
        // 外围会保证第一个发起迁移的线程调用此方法时, 参数nextTab为null, 之后参与迁移的线程调用此方法时, nextTab不会为null
        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是ConcurrentHashMap中的属性
            nextTable = nextTab;
            // transferIndex也是ConcurrentHashMap中的属性, 用于控制迁移的位置
            transferIndex = n;
        }
        int nextn = nextTab.length;
        // ForwardingNode代表正在被迁移的Node
        // 这个构造方法会生成一个Node, 其key、value、next属性都为null, 同时会将hash设为 MOVED
        // 后面会看到, 原数组中位置i处的节点完成迁移工作后, 就会将位置i处设置为ForwardingNode, 用来告知其它线程该位置已经被处理过了, 所以它其实相当于是一个标志
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        
        // advance指的是做完了一个位置的迁移工作, 可以准备做下一个位置的了
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        
        // 这个for循环是最难理解的
        // i是位置索引, bound是边界, 从后往前
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            
            // advance为true代表可以进行下一个位置的迁移了
            // i指向了transferIndex, bound指向了transferIndex - stride
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                
                // 将transferIndex赋值给nextIndex
                // 当transferIndex一旦小于等0, 说明原数组的所有位置都有相应的线程去处理了
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    // nextBound是这次迁移任务的边界, 这里是从后往前
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    // 所有的迁移操作已经完成
                    nextTable = null;
                    // 将新的nextTab赋值给table属性, 完成迁移
                    table = nextTab;
                    // 重新计算sizeCtl: n是原数组的长度。 先将n左移1位, 相当于n * 2。然后将n做无符号位右移1位, 相当于n * 0.5。此时sizeCtl = 2n - 0.5n。所以此时sizeCtl为原数组长度的1.5倍
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                // 在迁移之前, 会将sizeCtl的值设置为((rs << RESIZE_STAMP_SHIFT) + 2)
                // 然后每有一个线程参与迁移就会将sizeCtl + 1
                // 这里使用CAS操作对sizeCtl进行减1,代表线程做完了属于自己的任务
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    // 任务结束, 方法退出
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    
                    // 如果代码执行到这里, 就说明(sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
                    // 说明当前所有迁移的任务都做完了, 也就会进入到上面的 if(finishing)分支
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 如果位置i处是空的, 没有任何节点, 那么放入刚刚初始化的ForwardingNode空节点
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // 该位置处是一个ForwardingNode代表该位置已经迁移过了
            else if ((fh = f.hash) == MOVED)
                // 修改状态, 已经迁移过了
                advance = true; // already processed
            else {
                // 对数组该位置处的节点加锁, 开始处理数组该位置的迁移工作
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        // 头节点的hash >= 0, 代表是链表的Node节点
                        if (fh >= 0) {
                            // 下面这一块和JDK 7中的ConcurrentHashMap中的迁移是一样的
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            // 先遍历原链表, 找到lastRun节点
                            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;
                            }
                            // 找到lastRun节点之后, 将lastRun节点之后的链表一起进行迁移
                            // lastRun之前的节点需要进行克隆, 将原来的一个链表拆分为两个链表
                            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);
                            }
                            // 将lastRun放到新数组的i位置
                            setTabAt(nextTab, i, ln);
                            // 将lastRun之前的元素放到新数组的i + n位置
                            setTabAt(nextTab, i + n, hn);
                            // 将原数组该位置设置为ForwardingNode, 代表该位置已经处理完毕
                            // 其它加入迁移的线程看到该位置的hash值为MOVED, 就会不再进行迁移
                            setTabAt(tab, i, fwd);
                            // 迁移完成
                            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;
                                }
                            }
                            // 如果将红黑树一分为二之后, 节点数少于8个, 那么将红黑树转换回链表
                            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;
                            // 将ln放置在新数组的i位置
                            setTabAt(nextTab, i, ln);
                            // 将hn放置在新数组的i + n位置
                            setTabAt(nextTab, i + n, hn);
                         	// 将原数组该位置设置为ForwardingNode, 代表该位置已经处理完毕
                            // 其它加入迁移的线程看到该位置的hash值为MOVED, 就会不再进行迁移
                            setTabAt(tab, i, fwd);
                            // 迁移完成
                            advance = true;
                        }
                    }
                }
            }
        }
    }

​ 说到底,transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)这个方法并没有实现所有的数据迁移任务,每次调用这个方法只是实现了transferIndex往前stride个位置的迁移工作,其它的需要由外围来控制。也就是说每次调用transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)方法其实只是迁移了原数组的一部分节点。

4.3get()过程

​ 无论是哪个版本的JDKget()方法都是最简单的。

  • 首先计算keyhash值。
  • 根据hashhash & (table.length - 1)找到数组对应位置的下标。
  • 根据该位置的节点类型进行查找。
    • 如果该位置为null,直接返回null即可。
    • 如果该位置的首节点正好是目标节点,直接返回节点的值即可。
    • 如果该位置节点的hash值小于0,说明数组的当前位置正在进行扩容的数据迁移,或者是红黑树。
    • 如果以上条件都不满足,那么就是链表,对链表进行遍历查找即可。
    /**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code key.equals(k)},
     * then this method returns {@code v}; otherwise it returns
     * {@code null}.  (There can be at most one such mapping.)
     *
     * @throws NullPointerException if the specified key is null
     */
    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        // table是否为null, 或者table数组上的该位置为null
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            // 如果数组该位置上的hash相等
            if ((eh = e.hash) == h) {
                // 如果key也相等, 直接返回value即可
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 如果hash小于0, 则有可能正在扩容迁移, 或者是红黑树
            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;
    }

​ 这里也是非常简单的,如果正好遇到数据正在扩容的情况,则是调用ForwardingNodefind()方法进行查找:

        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;
                // 如果key为null || table数组为null || table数组长度为0 || table数组的该位置的元素为null
                if (k == null || tab == null || (n = tab.length) == 0 ||
                    (e = tabAt(tab, (n - 1) & h)) == null)
                    // 直接返回null
                    return null;
                for (;;) {
                    int eh; K ek;
                    // 如果头节点就是目标元素
                    if ((eh = e.hash) == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        // 直接返回
                        return e;
                    // 如果头节点的hash值小于0
                    if (eh < 0) {
                        // 如果头节点的数据类型是ForwardingNode, 则说明还在执行扩容迁移
                        if (e instanceof ForwardingNode) {
                            tab = ((ForwardingNode<K,V>)e).nextTable;
                            continue outer;
                        }
                        // 否则就是红黑树
                        else
                            // 搜索红黑树查找结果
                            return e.find(h, k);
                    }
                    if ((e = e.next) == null)
                        return null;
                }
            }
        }
    }

​ 现在回过去看前面,读HashMapConcurrentHashMap源码也并不难。读源码并不是目的,关键是学习Doug Lea大师的编程思想。大师就是大师,能够将HashMapConcurrentHashMap01,确实是非常厉害,中间的各种编程思想,值得学习。


备注:此文为笔者学习Java的笔记,鉴于本人技术有限,文中难免出现一些错误,感谢大家批评指正。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值