HashMap & ConcurrentHashMap源码详解


本文主要解读 JDK1.8 的实现,部分内容会与 JDK1.7 的实现进行对比。另外ConcurrentHashMapHashMap的线程安全版本,其原理类似,故不会完全解读源码。

1.HashMap

1.1 JDK1.8中HashMap的实现

在JDK1.8中,HashMap是基于 数组+链表/红黑树 。

重要属性
硬背没意义,理解 自动扩容put 才是关键

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

// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

// 扩容因子
static final int DEFAULT_LOAD_FACTOR = 0.75f;

// 树化的阈值
static final int TREEIFY_THRESHOLD = 8;

// 树退化的阈值
static final int UNTREEIFY_THRESHOLD = 6;

// 树化数组最小值
static final int MIN_TREEIFY_CAPACITY = 64;

1.1.1 扩容原码

虽然有点长,但一定坚持要看完。

final Node[] resize() {
    // 原数组
    Node[] 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;
        }
        // 新数组容量 < 最大数组容量 并且 原数组容量 >= 16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 扩容为 2 倍
            newThr = oldThr << 1; // double threshold
    }
    // 原数组容量 = 0
    // 原数组扩容阈值 > 0
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    // 数组容量和数组扩容阈值都为0 说明没有初始化 没有传参 使用默认值
    else {               // zero initial threshold signifies using defaults
        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;
    // 忽略警告
    @SuppressWarnings({"rawtypes","unchecked"})
    // 创建新数组
    Node[] newTab = (Node[])new Node[newCap];
    table = newTab;
    // 原数组不为空 说明有元素 那就需要移动到新数组中
    if (oldTab != null) {
        // 遍历原数组
        for (int j = 0; j < oldCap; ++j) {
            Node e;
            // 存在元素 且用e保存起来
            if ((e = oldTab[j]) != null) {
                // 置空数组中当前下标位置元素 便于垃圾回收
                oldTab[j] = null;
                // 单个元素直接移动
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 树化的结构走红黑树移动逻辑
                else if (e instanceof TreeNode)
                    // split方法中包含树退化的逻辑
                    ((TreeNode)e).split(this, newTab, j, oldCap);
                // 链表结构移动元素  重要!!!
                else { // preserve order
                    // 这里引入了高低位指针
                    Node loHead = null, loTail = null;
                    Node hiHead = null, hiTail = null;
                    Node 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;
                    }
                    // 移动元素
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

自动扩容中有几个显而易见的设计是非常巧妙的。

容量始终是2的指数次幂,这样设计在resize()中看来有两个好处:

  1. 扩容直接使用位运算newThr = oldThr << 1;,效率更高(忽略),代码优雅。
  2. 在移动元素的时候,和高低位指针的设计完美结合,不需要二次hash。

高低位指针本身就很巧妙,即不二次hash,又完成了散列。

1.1.2 添加元素源码

// 虽然说put本身只调用putVal 但是它有2个写死的参数需要了解
public V put(K key, V value) {
    // 第三个参数:onlyIfAbsent – 如果为真,则不更改现有值。很明显,我们使用HashMap时put相同的key会覆盖,这里false也印证了
    // 第四个参数:evict – 如果为 false,则表处于创建模式。 用到这个参数的方法是一个空方法,所以不用管
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; Node p; int n, i;
    // 如果数组为空 或 长度为零
    if ((tab = table) == null || (n = tab.length) == 0)
        // 调用resize方法初始化数组
        n = (tab = resize()).length;
    // 通过将要put的元素的hash确定元素下标位置 并判断该位置是否有元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 为空,则说明没有元素,直接加入新节点即可
        tab[i] = newNode(hash, key, value, null);
    // 不为空 说明出现了hash冲突
    else {
        Node e; K k;
        // 判断要put元素的hashcode 及 key 是否与该位置已有元素相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 相同则赋值给e
            e = p;
        // 看该位置元素是否为红黑树
        else if (p instanceof TreeNode)
            // 走红黑树的put逻辑
            e = ((TreeNode)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);
                    // binCount >= 7,第8次循环,但上一行已经添加了一个元素,所以是数组元素超过8才树化
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 树化
                        treeifyBin(tab, hash);
                    // next为空说明遍历结束
                    break;
                }
                // 走过上面的代码说明 e 不是末尾节点
                // put的元素和对应位置的元素的key,key的hashcode是否相同
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // p 指向下一个节点
                p = e;
            }
        }
        // 这里 e != null,走了上面 else逻辑
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 这个条件的前半段一定为 true
            if (!onlyIfAbsent || oldValue == null)
                // 替换值
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 数组占用空间超过阈值
    if (++size > threshold)
        // 扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}

1.1.3 获取值

// 同put方法,门面
public V get(Object key) {
    Node e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node getNode(int hash, Object key) {
    Node[] tab; Node first, e; int n; K k;
    // 数组不为空 && 数组长度大于0 && 传入的key的hash值与上数组长度-1不为空(找到下标且元素不为空)
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 检查传入key的hash是否与该下标元素hash码相同
        if (first.hash == hash && // always check first node
            // key内容是否相同
            ((k = first.key) == key || (key != null && key.equals(k))))
            // 返回节点
            return first;
        // 说明一个下表存了多个节点 即出现了碰撞
        if ((e = first.next) != null) {
            // 如果是红黑树
            if (first instanceof TreeNode)
                return ((TreeNode)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;
}

1.1.4 链表树化

final void treeifyBin(Node[] tab, int hash) {
    int n, index; Node e;
    // 如果数组长度<64 则会对数组进行扩容 而不是树化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 计算下标位置并获取元素
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode hd = null, tl = null;
        do {
            // 链表节点 -> 红黑树节点
            TreeNode p = replacementTreeNode(e, null);
            // 遍历第一次记录红黑树根节点
            if (tl == null)
                hd = p;
            // 这里即之后的逻辑组织好了新的连接关系
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // 之前的循环体结束
        if ((tab[index] = hd) != null)
            // 真正构建红黑树的逻辑
            hd.treeify(tab);
    }
}
// 构建红黑树
// 需要注意的是treeify是HashMap类的静态内部类的方法,不然看不懂下面的this
final void treeify(Node[] tab) {
    TreeNode root = null;
    // 遍历逻辑 这里的this实际上是调用该方法的节点
    for (TreeNode x = this, next; x != null; x = next) {
        // 下一个节点
        next = (TreeNode)x.next;
        // x的左右置空
        x.left = x.right = null;
        // 如果还没有根节点
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        // 根节点已存在的逻辑
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            // 从根节点开始查找
            for (TreeNode p = root;;) {
                int dir, ph;
                K pk = p.key;
                // 这里要注意之前逻辑的赋值操作
                if ((ph = p.hash) > h)
                    dir = -1;  // 向左查找
                else if (ph < h)
                    dir = 1;   // 向右查找
                // 相等则判断key
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode xp = p;
                // 这里dir就是选择左还是右的依据
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    // 进行红黑树的插入平衡
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

1.1.5 红黑树退化

final Node untreeify(HashMap map) {
    Node hd = null, tl = null;
    for (Node q = this; q != null; q = q.next) {
        // 生成链表节点
        Node p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

1.1.6 删除元素

public V remove(Object key) {
    Node e;
    // 参数说明参考下面的原函数
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}
// value: 调用方默认为空
// matchValue:调用方默认为false 配合value使用
// movable:调用方默认为true 表示删除元素时会移动其它元素
final Node removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node[] tab; Node p; int n, index;
    // 数组不为空 && 数组长度不为0 && 传入key定位的下标位置存在元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        // 声明一堆局部变量
        Node node = null, e; K k; V v;
        // 判断要删除的key的hash 和 找到数组对应下标元素(头)的hash 是否相同 && key内容是否相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;  // 记录
        else if ((e = p.next) != null) {
            // 红黑树删除
            if (p instanceof TreeNode)
                node = ((TreeNode)p).getTreeNode(hash, key);
            // 链表删除节点
            else {
                do {
                    // 同头节点判断
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // node != null 说明要删除的key是存在的
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode)node).removeTreeNode(this, tab, movable);
            // mode == p 只会是头节点 对应上面的if逻辑
            else if (node == p)
                tab[index] = node.next;
            // 移除链表节点
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

1.2 JDK1.8 HashMap核心原理小结

1.2.1 底层数据结构

数组 + 链表 + 红黑树

1.2.2 自动扩容原理

  • 创建一个2倍容量的新数组
  • 如果是红黑树则调用红黑树的逻辑;如果是链表则引入高低位指针进行移动和散列

1.2.3 put流程

  • 先检查数组是否初始化,未初始化则调用resize()方法初始化
  • 如果对应下表为空则直接创建节点加入
  • 如果对应下表是红黑树则走红黑树的插入逻辑
  • 如果是链表,则遍历,如果有相同的key则替换,没有则在末尾追加
    • 在遍历链表的时候会检查链表长度(实质是长度=9会树化),如果长度超过8则会进行树化判断treeifyBin(),如果数组容量达到64则树化,反之优先扩容数组resize()

1.2.4 get流程

get比较简单,根据key去定位数组下标,再遍历即可。

1.2.5 remove流程

  • 首先要检查给定key是否存在,不存在直接结束方法
  • 如果要删除的元素在红黑树结构中,会先获取树节点,再removeTreeNode,这里也会检查树是否退化
  • 如果要删除的元素再链表结构中,先获取链表节点,再删除

1.2.6 关于HashMap自带属性的默认值

  • 初始容量16:没有为啥,设计者就这么写的
  • 最大容量 1 << 30:计算机限制 Integer.Max_Value
  • 扩容因子 0.75f:牛顿二项式推到出来的最优值越0.69,Java取大了一点,应该是出于时间和空间平衡的考虑
  • 树化阈值8 & 退化阈值6:时间复杂度
  • 链表树化数组最小容量64:泊松分布算的,概率问题

1.3 JDK1.8 和 JDK1.7中HashMap的一点区别

JDK1.7中HashMap采用的是 数组 + 链表 的结构实现的。另外,再扩容方面,JDK1.7是对链表进行遍历,然后计算hashcode重新散列到新数组,并且在移动的过程中采用的是头插法,即新老数组中链表结构会反转。

JDK1.7中扩容采用的头插法,在并发场景下会出现问题,就是链表成环,导致后续无法put元素,或者get,因为会进入死循环。

2.ConcurrentHashMap

ConcurrentHashMapHashMap原理类似,只是它考虑了线程安全问题,使用了synchronized和一些CAS的方法

2.1 JDK1.8 ConcurrentHashMap

其内部维护的很多变量和HashMap一样。

数据结构:
synchronized + CAS + Node + 红黑树

锁:

  • 读操作无锁,volatile保证,修饰Node的val和next。
  • 写操作加锁,锁链表的头节点,不影响其它元素的读写,锁的粒度更细,效率更高。
  • 扩容时阻塞所有读写操作,并发扩容

2.2 JDK1.7中ConcurrentHashMap

采用分段锁(Segment)的方式来实现加锁。一个Hash表中套一个Hash表。锁的粒度大。

数据结构:
ReentrantLock + Segment + HashEntry
Segment是一个HashEntry的数组,每个元素都是链表结构(或空)。

元素查询:
需要两次hash,第一次查询Segment,第二次定位到元素链表的头部
get方法无需加锁,volatile保证

锁:
Segment继承了ReentrantLock。加锁会锁定整个Segment,锁粒度大,但是不会影响其它Segment。并发度等于Segment数,扩容不会影响其它Segment。每个Segment内部会进行扩容。