java 集合源码分析1

130 阅读25分钟

ArrayList 源码分析

ArrayList 简介?

  • ArrayList 底层是一个 Object[] 数组,它的容量可以动态增长;
  • ArrayList 继承于 AbstractList,实现了 List、RandomAccess、Cloneable、Serializable 接口,这表明:
    • List 接口表明 ArrayList 是一个列表,支持添加、删除、查找等操作,并且支持通过下标进行访问;
    • RandomAccess 接口表明 ArrayList 支持快速随机访问,即我们可以通过元素的序号快速获取到元素的对象;
    • Cloneable 接口表明,ArrayList 具有拷贝能力;
    • Serializable 接口表明,它可以进行序列化和反序列化操作,即对象可以转为字节流进行持久化存储或网络传输;

ArrayList 参数介绍?

  • 默认初始容量大小是 10;
  • Object[] 数组类型空静态变量 EMPTY_ELEMENTDATA 共享给所有初始化参数指定为 0 的 ArrayList 对象;
  • Object[] 数组类型空静态变量 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 共享给所有没有指定初始化容量的 ArrayList 对象;
  • 两个 Object[] 数组类型空静态变量是为了区分这两种空 ArrayList 第一次添加数据时需要初始化数组长度的容量,对于没有指定初始容量的 ArrayList 下次扩容为默认初始容量 10;
  • 一个非私有的 transient Object[] elementData 数组用来保存数据,非私有是为了简化扩展类对 elementData 的访问;而 transient 是为了让 ArrayList 不序列化整个数组,只序列化具体的元素,当反序列化的时候,需要重新创建数组,这样效率更高;

构造函数简介?

一共有三个构造函数:

  1. 带初始容量参数的构造函数:
    • 如果初始容量大于 0 ,创建指定初始化容量的数组;
    • 如果初始化容量等于0 ,将 EMPTY_ELEMENTDATA 赋值给 elementData 数组;
    • 其他情况抛出参数异常错误;
  2. 无参构造函数:
    • 将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给 elementData 数组;
  3. 指定一个集合参数的构造函数:
    • 将指定的集合转换为数组并赋值给 elementData;
    • 如果 elementData 数组的长度不为 0, 判断数组的类型,如果不是 Object 类型,使用 Arrays.copyOf() 将 elementData 数组类型转换为 Object;
    • 如果 elementData 数组的长度为 0,将 EMPTY_ELEMENTDATA 赋值给 elementData;

trimToSize() 的作用?

将 ArrayList 的容量大小 resize 为实际大小,从而最小化 ArrayList 的实例的存储;

ArrayList 的 clone()?

ArrayList 的 clone() 是浅克隆,使用 Arrays.copyOf(elementData, size); 复制一个新的数组, 但是数组的元素是共用的;

扩容过程分析?

以无参构造创建了新的 ArrayList 作为起点进行分析。当无参构造 new ArrayList 后,会将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给 elementData,当第一次向 ArrayList 添加元素触发扩容。

// 第一步:调用 add 添加元素
public boolean add(E e) {
    // 加元素之前,先调用ensureCapacityInternal方法
    ensureCapacityInternal(size + 1); 
    // 这里看到ArrayList添加元素的实质就相当于为数组赋值
    elementData[size++] = e;
    return true;
}

// 第二步:确保内部容量达到指定的最小容量
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 第三步:根据给定的最小容量和当前数组元素来重新计算所需容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果当前数组元素为空数组(初始情况),返回默认容量和最小容量中的较大值作为所需容量
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 否则直接返回最小容量
    return minCapacity;
}

//第四步:根据所需容量判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果所需容量大于当前容量,则需要进行扩容
    if (minCapacity - elementData.length > 0)
        // 调用grow方法进行扩容
        grow(minCapacity);
}

// 第五步:扩容
private void grow(int minCapacity) {
    // oldCapacity为旧容量,newCapacity为新容量
    int oldCapacity = elementData.length;
    // 将oldCapacity 右移一位,其效果相当于oldCapacity /2,
    // 我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
    // 如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 处理新的容量大于最大容量的情况
private static int hugeCapacity(int minCapacity) {
    // 当 minCapacity < 0 表示发生了溢出,直接报错
    if (minCapacity < 0) 
        throw new OutOfMemoryError();
    // 对 minCapacity 和 MAX_ARRAY_SIZE 进行比较:
    // 若 minCapacity 大,将 Integer.MAX_VALUE 作为新数组的大小
    // 若 MAX_ARRAY_SIZE 大,将 MAX_ARRAY_SIZE 作为新数组的大小
    // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

对于无参构造函数构造的 ArrayList 扩容过程?

  1. 创建新的 ArrayList 对象,并将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给 elementData;
  2. 调用 add 方法添加元素,此时发现 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,将 elementData 扩容到默认的 10;
  3. 依次 add 元素,直到第 11 个元素,此时进行扩容为 15,(old + (old >> 1)),然后继续添加;

对于非无参构造函数构造的 ArrayList 库容过程?

每次都会进行扩容判断,根据实际情况进行扩容;

ArrayList 的 ensureCapacity 方法?

这个方法是提供给外部调用的,其目的是在需要大量向 ArrayList 添加元素时,可以调用该函数,让个 ArrayList 提前库容完成,避免添加元素期间多次扩容;

LinkedList 源码分析

LinkedList 简介?

LinkedList 继承了 AbstractSequentialList,而 AbstractSequentialList 又继承自 AbstractList; LinkedList 实现的接口:

  • List: 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
  • Deque: 继承自 Queue 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。
  • Cloneable: 表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
  • Serializable: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输。

LinkedList 的 Node 定义?

private static class Node<E> {
    E item;// 节点值
    Node<E> next; // 后继节点
    Node<E> prev; // 前驱结点

    // 初始化参数顺序分别是:前驱结点、本身节点值、后继节点
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList 的构造函数?

// 无参构造函数, 构造空链表
public LinkedList() {
}

// 接收一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList 插入元素?

  • add(E e) 插入链表尾部;
  • add(int index, E element) 在链表指定位置插入;
    • 判断 index 是否为链表尾,如果是采用 linkLast 插入;
    • 如果不是,采用 linkLast 插入到 index 指定元素之前;

插入尾部会直接调用 linkLast(E e) 方法:

// 将元素节点插入到链表尾部
void linkLast(E e) {
    // 将最后一个元素赋值(引用传递)给节点 l
    final Node<E> l = last;
    // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
    final Node<E> newNode = new Node<>(l, e, null);
    // 将 last 引用指向新节点
    last = newNode;
    // 判断尾节点是否为空
    // 如果 l 是null 意味着这是第一次添加元素
    if (l == null)
        // 如果是第一次添加,将first赋值为新节点,此时链表只有一个元素
        first = newNode;
    else
        // 如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next
        l.next = newNode;
    size++;
    modCount++;
}
// 在指定元素之前插入元素
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;断言 succ不为 null
    // 定义一个节点元素保存 succ 的 prev 引用,也就是它的前一节点信息
    final Node<E> pred = succ.prev;
    // 初始化节点,并指明前驱和后继节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 将 succ 节点前驱引用 prev 指向新节点
    succ.prev = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点
    if (pred == null)
        first = newNode;
    else
        // succ 节点前驱的后继引用指向新节点
        pred.next = newNode;
    size++;
    modCount++;
}

LinkedList 获取元素?

  • getFirst():获取链表的第一个元素。
  • getLast():获取链表的最后一个元素。
  • get(int index):获取链表指定位置的元素。

对于获取指定位置的的元素核心方法:

// 返回指定下标的非空节点
Node<E> node(int index) {
    // 如果index小于size的二分之一  从前开始查找(向后查找)  反之向前查找
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 遍历,循环向后查找,直至 i == index
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

HashMap 源码分析

HashMap 简介?

  • HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的;
  • HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;
  • JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
  • HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。

HashMap 的属性介绍?

    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶上的结点数大于等于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当桶上的结点数小于等于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table;
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;
    // 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容
    // threshold = capacity * loadFactor,当 Size>threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准。
    int threshold;
    // 负载因子
    // loadFactor 负载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。
    //loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值。
    // 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量超过了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。
    final float loadFactor;

HashMap Node 节点类:

static class Node<K,V> implements Map.Entry<K,V> {
       // 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
       final int hash;
       //键
       final K key;
       //值
       V value;
       // 指向下一个节点
       Node<K,V> next;
}

HashMap 树节点:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        // 父
        TreeNode<K,V> parent;  
        // 左
        TreeNode<K,V> left;    
        // 右
        TreeNode<K,V> right;   
        // needed to unlink next upon deletion
        TreeNode<K,V> prev;    
        // 判断颜色
        boolean red;           
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        // 返回根节点
        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
    }
}

HashMap 的四个构造方法介绍?

  • HashMap() 默认构造函数,装载因子为默认的 0.75f;
  • HashMap(Map<? extends K, ? extends V> m) 包含一个 map 的构造函数, 默认装载因子为 0.75f,并且会调用 putMapEntries(m, false); 方法把 map 全部放到新的map中;
  • HashMap(int initialCapacity) 指定容量大小的构造函数;
  • HashMap(int initialCapacity, float loadFactor) 指定容量大小和装载因子的构造函数;

对于指定了容量大小的构造函数,只是将指定的容量大小通过 tableSizeFor 计算出阈值,在后续扩容的过程中才会真正初始化;

putMapEntries 方法分析?

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        // 判断table是否已经初始化
        if (table == null) {
            /*
             * 未初始化:
             * 阈值=容量*负载因子,ft指的是要添加s个元素所需的最小的容量;
             */
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);
            /*
             根据构造函数可知,table未初始化,threshold实际上是存放的初始化容量,如果添加s个元素所需的最小容量大于初始化容量,则将最小容量扩容为最接近的2的幂次方大小作为初始化。
             如果容量达到最大值 1>>>30, 则返回最大值,否则扩大为原来的二次幂。
             */
            if (t > threshold)
                threshold = tableSizeFor(t);
        // 已初始化,并且m元素个数大于阈值,进行扩容处理
        } else if (s > threshold){
            resize();
        }
        // 将m中的所有元素添加至HashMap中,如果table未初始化,putVal中会调用resize初始化或扩容
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}

put 方法分析?

put 方法的大致步骤:

  • 如果定位到数组位置没有元素就直接插入;如果定位到数组位置有元素,就比较 key 是否相同,如果相同就替换值;如果不同,就判断当前节点是否为树节点,如果是树节点,按照树节点插入;如果是不是树节点就插入链表尾部,插入后判断是否满足转树的条件,如果满足就转树,这个插入过程完成后,对 size 进行自增后,判断是否需要扩容,入股需要扩容就进行扩容。

源码详细分析

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

// putVal 方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // table 未初始化或者长度为 0,进行扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素(处理hash冲突)
    else {
        Node<K,V> e; K k;
        // 快速判断第一个节点table[i]的key是否与插入的key一样,若相同就直接使用插入的值p替换掉旧的值e。
        if (p.hash == hash && ((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);
                    // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
                    // 这个方法会根据 HashMap 数组来决定是否转换为红黑树。
                    // 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是对数组扩容。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循环
                    break;
                }
                // 判断链表中结点的key值与插入的元素的key值是否相等
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循环
                    break;
                // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                p = e;
            }
        }
        // 表示在桶中找到key值、hash值与插入元素相等的结点
        if (e != null) {
            // 记录e的value
            V oldValue = e.value;
            // onlyIfAbsent为false或者旧值为null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替换旧值
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 结构性修改
    ++modCount;
    // 实际大小大于阈值则扩容
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

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;
    // table 不为 null, 长度不为 0,hash 所在的 hash 桶位置不为 null。
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // 检查 hash 所在位置的第一个元素 key 是否相同,相同就返回对应的值
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 第一个 key 不是要找的key,且桶中不止一个节点;
        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;
}

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;
    // 旧容量大于 0
    if (oldCap > 0) {
        // 超过最大值就不再扩充了,就只好随你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        // 将旧容量翻倍作为新容量,如果没超过最大值,就扩充为原来的2倍
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;
    // 旧阈值大于 0,(说明此时 oldCap == 0,说明此时是创建对象后的初始化阶段)
    }else if (oldThr > 0)
        // 创建对象时初始化容量大小放在threshold中,此时只需要将其作为新的数组容量
        newCap = oldThr;
    // 默认情况,旧容量和旧阈值都为 0,,此时按照无参构造函数创建的对象在这里计算容量和阈值
    else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // 创建时指定了初始化容量或者负载因子,在这里进行阈值初始化,
    	// 或者扩容前的旧容量小于16,在这里计算新的resize上限
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 把每个旧的bucket都移动到新的bucket中
        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)
                /**
                 * 1. 对于树,会根据树的每个节点的hashcode最高位是 0 或者 1,拆分为 2 棵树;
                 * 2. 最高位是 0 的树,会在原来的位置,最高位是 1 的,会放在新位置(index + oldCap)
                 * 3. 将红黑树拆分成2棵子树,如果子树节点数小于等于 UNTREEIFY_THRESHOLD(默认为 6),则将子树转换为链表。
                 * 4. 如果子树节点数大于 UNTREEIFY_THRESHOLD,则保持子树的树结构。
                 * 
                 * 举例帮助理解:对于长度为 2 的 table,2 和 4 都会分配到 2 位置,当 table 扩容后,2 应该保持位置不变,但是 4 应该去 4 位置。
                 */
                    ((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;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

remove 方法源码详解?

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
    }

    final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        // 首先,table 不为空、table 对应的位置桶位置不为空;
        if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            // 如果头结点就是删除的节点,把头节点保存到 node 里;
            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<K,V>)p).getTreeNode(hash, key);
                else {
            // 如果是链表,遍历链表,找到要删除的点,此时 node 保存的是要删除的节点的前一个节点,p 保存的是要删除的节点;
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            // 如果要删除的节点存在;
            if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            // 对于树,按照树删除;
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
            // 对于头结点就是要删除的节点,要删除的节点的 next 节点放到桶里;
                    tab[index] = node.next;
                else
            // 对于链表,将要删除的节点的前置节点的 next 指针指向要删除的节点的 next 节点;
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

ConcurrentHashMap 源码分析

ConcurrentHashMap 1.7

ConcurrentHashMap 1.7 的存储结构?

  • 结构:Segment 数组 + HashEntry 数组 + 链表;
  • Segment 数组不能扩容,默认长度是 16,由于锁加在每个 Segment 上,所以这意味着默认支持 16 个线程并发;
  • HashEntry 数组可以扩容,扩容是基于装载因子(默认 0.75f),并将 HashEntry 扩容为原来的 2 倍;
  • Segment 和 HashEntry 的长度都是 2 的次幂;

ConcurrentHashMap 1.7 的构造函数?

  • 无参构造函数,默认初始化容量是 16, 装载因子是 0.75f,并发级别是 16;
  • 有参构造函数:
    • 如果并发级别超过 MAX_SEGMENTS(65536),就取最大值 MAX_SEGMENTS;
    • 否则取指定 initialCapacity 最近的 2 次幂数作为 initialCapacity;
    • 计算 segmentShift 偏移量,segmentMask 掩码;
    • 计算 每个 segment 的容量,公式: 容量/并发级别 = 单个 segment 容量,默认情况下是 16/16 = 1, 由于容量必须是 2 或者 2 的倍数,所以初始容量是 2;
    • 初始化 segments[0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。

ConcurrentHashMap 1.7 的 put 方法详解?

  1. 通过 hash 值与 segmentShift 偏移量和 segmentMask 的掩码进行计算,获取到当前 key 所在的 segment;
  2. 如果当前 segment 为 null,调用 ensureSegment 对当前 segment 进行初始化;
  3. 最后调用 segment 的 put 方法存放元素;

ConcurrentHashMap 1.7 的 segment 初始化方法 ensureSegment 详解?

  1. 首先判断指定位置的 segment 是否为 null,如果不为 null 直接返回 segment;
  2. 当 segment 为 null 时,获取 0 号 segment 的容量、装载因子,并计算阈值;
  3. 根据前面获取的容量、装载因子、阈值初始化 HashEntry 数组;
  4. 再次判断当前位置的 segment 是否为 null, 如果为 null,采用 cas 自旋来对当前位置 segment 进行赋值,完成初始化;

ConcurrentHashMap 1.7 的 segment 的 put 方法详解?

  1. Segment 继承了 ReentrantLock,所以 Segment 内部可以很方便的获取锁;
  2. tryLock() 获取锁,获取失败,则使用 scanAndLockForPut 方法继续获取锁;
  3. 获取到锁后,根据 hash 和 tab.length 计算出当前 key 所在 HashEntry 数组的位置,并获取当前的 HashEntry;
  4. 如果当前 HashEnty 不为 null,就遍历链表,如果找到相同的 key 就替换值;
  5. 如果没有找到相同的 key,判断 node 是否为 null,如果 node 为 null ,就用 key、value 新建一个 HashEntry ,如果 node 不为 null,将 node 指向 头结点,使用头插法插入;
  6. 然后判断是否需要扩容,进行处理;
  7. put 结束,释放锁;

ConcurrentHashMap 1.7 的 segment 的 scanAndLockForPut 方法详解?

  1. 进入 scanAndLockForPut 方法后,先采用 cas 自旋进行 tryLock;
  2. 达到一定次数后,进入阻塞获取锁状态 lock;
  3. 获取到锁,方法返回;

ConcurrentHashMap 1.7 的扩容 rehash 方法详解?

  1. 首先根据 oldTable.length 把容量扩充为 2 倍;
  2. 计算出新的阈值;
  3. 创建新的 HashEntry 数组;
  4. 循环老的 HashEntry 数组,将元素重新分配到新的 HashEntry 数组中;
    • 首先获取老 HashEntry[i]的值,如果它的 next 为 null,表示是单值,直接计算在新数组中的位置并放置;
    • 如果 next 不为 null,首先循环整个链表,找到最后一个不在原位置的元素,并把它放到新的位置;
    • 再次循环整个链表,根据每个元素位置,将它们链接到新的链表上;
  5. 计算传入的 node 在新数组中的位置,并放到正确的位置;
  6. 把新数组赋值给 table;

ConcurrentHashMap 1.7 的 get 方法详解?

get 方法分两步查询即可;

  1. 通过 hash 以及 segmentShift 和 segmentMask,计算出 segment 的位置;
  2. 从 segment 中取出 HashEntry,并计算 key 在 HashEntry 中的位置;
  3. 遍历 HashEntry, 根据 hash 值相等,及 key equals 取出正确的值,如果没有,返回 null;

ConcurrentHashMap 1.8

ConcurrentHashMap 1.8 的存储结构?

不再是 segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表/红黑树,当链表达到一定长度,就会转换成红黑树;

2. ConcurrentHashMap 1.8 初始化 initTable 详解?

由于 ConcurrentHashMap 构造函数只会先创建一个空的对象,不会立即对 Node 数组进行初始化化,当需要使用是,才会调用 initTable 进行初始化;

初始化的过程如下:

  1. 进入自旋,自旋条件是 Node<K,V>[] tab 是否为 null 或者 长度为 0;
  2. 在自旋中判断 sizeCtl 是否小于 0, 小于 0 表示另外的线程正在对 tab 初始化或者扩容,此时需要调用 Thread.yield(); 让出 CPU;
  3. 否则使用 unsafe 类的 compareAndSwapInt 将 SIZECTL 位置的 sizeCtl 字段值更新为 -1,如果更新失败,进入下次自旋,如果更新成功,对 tab 进行初始化;
  4. 根据初始化的容量计算出最新的 sizeCtl 作为下次扩容的阈值;
  5. 返回 tab;

源码

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    // 开始自旋
    while ((tab = table) == null || tab.length == 0) {
        // 如果 sizeCtl < 0 ,说明另外的线程执行CAS 成功,正在进行初始化或者扩容;
        if ((sc = sizeCtl) < 0)
            // 让出 CPU 使用权
            Thread.yield(); 
        // 尝试 cas,当 SIZECTL 位置的字段 sizeCtl 值等于 sc 时,更新为 -1;
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 计算新的 sizeCtl 作为下次扩容的阈值,实际值是 n*(3/4);
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

sizeCtl 值说明:

  • -1 说明正在初始化,其他线程需要自旋等待;
  • -N (N - 1) 为正在进行扩容的线程数;
  • 0 表示 table 初始化大小, table 没有初始化;
  • 0 表示 table 下次扩容的阈值;

2. ConcurrentHashMap 1.8 put 方法详解?

  1. 判断 key、value 是否为 null ,为 null 抛异常;
  2. 计算 key 在哈希桶中的位置;
  3. 自旋 table:
    • 如果 table 为 null 进入 initTable 流程,然后继续自旋;
    • 判断 table 当前位置是否为 null, 如果是 就 cas 尝试将新节点放入,成功就退出自旋,否则继续自旋;
    • 如果 table 当前位置是不为 null, 则判断当前 node 是否处于 MOVED 状态,如果处于移动状态,当前线程会进行帮助转移;
    • 如果当前 node 不是处于 MOVED 状态,则使用 synchronized 加锁,将节点插入;
      • 对于链表的情况,使用 for 循环,一边判断是否需要覆盖节点,一边判断是否到达链表尾部,如果到达尾部,直接插入;
      • 对于红黑树,直接按照红树插入;
public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key 和 value 为空抛异常
    if (key == null || value == null) 
        throw new NullPointerException();
    // 计算 key 在通中的位置
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 进入自旋
    for (Node<K,V>[] tab = table;;) {
        // f = 目标位置元素
        // fh 后面存放目标位置的元素 hash 值
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            // 数组桶为空,初始化数组桶(自旋+CAS)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 桶内为空,CAS 放入,不加锁,成功了就直接 break 跳出
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break; 
        } else if ((fh = f.hash) == MOVED)
            // 扩容时,正在发生元素迁移,需要进行帮助迁移
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 使用 synchronized 加锁加入节点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // 链表
                    if (fh >= 0) {
                        binCount = 1;
                        // 循环加入新的或者覆盖节点
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                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) {
                        // 红黑树
                        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) {
                // 插入成功后,检查是否需要扩容或者转树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

ConcurrentHashMap 1.8 get 方法详解?

  • 根据 hash 值计算位置。
  • 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
  • 如果头节点 hash 值小于 0 ,说明是红黑树。
  • 如果是链表,遍历查找之。
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // key 所在的 hash 位置
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 如果指定位置元素存在,头结点hash值相同
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                // key hash 值相等,key值相同,直接返回元素 value
                return e.val;
        }
        else if (eh < 0)
            // 头结点hash值小于0,说明正在扩容或者是红黑树,find查找
            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;
}