深入 Java 集合框架

一. Map

1. HashMap

HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。

1.1 存储结构

1.1.1 Node 结点

HashMap是数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的 ,当链表长度超过8 并且数组长度超过64,就会将链表转化成红黑树提高查询效率。

阅读源码可知,transient Node<K,V>[] table; 是存储内容的重要字段,所以要理清 Node 是什么

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
  • Node 是HashMap 中的静态内部类,用于存储键值对
  • 字段存储了该节点的 hash 值,key 和 value 值 以及下一个结点引用
  • 重写了hashCode() 方法 和 equals () 方法。

为什么重写 equals 时必须重写 hashCode 方法?

  1. 哈希表通过 hashCode 确定桶下标位置来减少 equals 的判断。提高哈希表的存取速度。
  2. 如果没有重写 hashCode 那么,所有对象都会调用Object 的hashCode 方法,即所有对象的 hashCode 都是不一样的,即使它们指向的是同样的数据。
  3. 另外规定,hashCode 相等,equals 不一定相等,equals 相等,hashCode则一定相等。不重写hashCode显然违背该特性。
  4. 如果不重写 hashcode 则在存储的时候不会通过 equals 判断直接加进哈希表,导致存入很多相同数据的对象,取出的时候,则 hashcode 无法定位准确的位置。
1.1.2 其他关键字段
哈希桶数组初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

为什么容量必须是 2 的 n 次幂

  • 为了减少哈希冲突的权衡
  • 算取 桶索引的算法是 :(n - 1) & hash 在 n 是 2的n次幂的情况下,实际就是hash % n 这样可以有效减少哈希冲突,并且位移运算 速率快于直接取模
  • 2的n次方实际就是1后面n个0,2的n次方-1实际就是n个1;

如果构造函数传入的 初始容量参数不是 2 的 n 次方

    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

会经过这个算法将初识容量转换成大于 传入参数的 最小 2的次方

在这里插入图片描述

加载因子和容纳键值对个数
     int threshold;             // 所能容纳的key-value对极限 
     
     final float loadFactor;    // 负载因子

首先,Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

结合负载因子的定义公式可知,threshold就是在此Load factor和length(数组长度)对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。

默认树化大小
static final int TREEIFY_THRESHOLD = 8;	// 默认树化大小 

static final int UNTREEIFY_THRESHOLD = 6; // 默认转换回链表大小

TreeNode 占用空间是普通 Node 的两倍,所以只有当bin包含足够多的节点时才会转成TreeNodes,而是否足够多就是由TREEIFY_THRESHOLD的值决定的。当bin中节点数变少时,又会转成普通的bin。并且我们查看源码的时候发现,链表长度达到8就转成红黑树,当长度降到6就转成普通bin。(8是采用泊松分布运算取得,因为通过散列的哈希值分担之后,每个桶中会有8个元素的概率只有0.00000006)

1.2 Java 7 HashMap 的主要问题

  • ① 扩容产生死循环的问题

具体可以看下面这篇博客

疫苗:JAVA HASHMAP的死循环

简单总结来说就是:

  1. 当两个线程同时进行 resize 的时候,会创建两个扩容的桶数组进行扩容。
  2. 如果第一个线程执行的比较快,在第二个线程 transfer 之前完成了整个 transfer 的操作,因为采用的是头插法的操作,所以相应的链表结构会产生一个倒置的现象,再继续通过同样顺序的头插法进行插入,则会产生死循环的问题。
  • ② 第二个问题就是采用大量的hashCode相同的键会导致哈希表退化为链表。

1.3 主要方法实现

1.3.1 确定 hash 值 及 哈希桶数组索引
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
	i = (n - 1) & hash
  1. 取得key 的hashCode
  2. 将hashCode h 和 h 向右移动 16位异或
  3. 之后和长度 n - 1 按位与运算 获得索引值

在这里插入图片描述

这个方法非常巧妙,它通过h & (table.length -1)来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率,并且将负数转化成了整数,减少了哈希冲突。

在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,hash 值改动不大时,也能保证考虑到高低位都参与到Hash的计算中,减小哈希冲突,同时不会有太大的开销。

1.3.2 put添加方法
    // 调用的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) {
        // 定义一些局部变量
        HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
        // 判断是否是第一次 put,是否分配过桶数组,没有则进行resize
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // i = (n - 1) & hash 通过该方法计算加入节点的桶下标
        // 如果该下标没有数据,则直接插入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            HashMap.Node<K,V> e; K k;
            // 如果存在相同的 key 则直接覆盖节点
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果没有相同的并且,该部分已经变成红黑树,则进入红黑树的插入操作
            else if (p instanceof HashMap.TreeNode)
                e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            // 没有变成红黑树的情况
            else {
                // binCount 代表节点个数,表示遍历节点
                for (int binCount = 0; ; ++binCount) {
                    // 如果下一个是 null 直接插入,代表是尾插
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果插入后达到树化阈值,则进行树化
                        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;
                }
            }
            // 这主要是帮助 LinkedHashMap 维护一些顺序
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 内部改动次数 + 1
        // 该值保证出现并发问题的时候出发 fast-fail抛出异常
        ++modCount;
        // 如果尺寸大于容量,则扩容
        if (++size > threshold)
            resize();
        // 该方法同样留给子类做一些操作
        afterNodeInsertion(evict);
        return null;
    }

总结流程:

  • 检查是否桶数组是否分配,如果没有则进入 resize();
  • 计算该key的桶下标,如果对应位置为空,则直接插入。
  • 如果对应的节点为树节点,则进入红黑树的插入过程。
  • 如果是链表,则遍历链表,如果找不到同样的 key 并且到达链表末尾,则插入(尾插),并且长度大于8可能会导致树化。
  • 如果找到相同的key则覆盖。
  • 添加完最后检查包含键值对的个数是否到达容量阈值,到达则进行 resize();
1.3.3 treeifyBin 树化方法
    final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
        int n, index; HashMap.Node<K,V> e;
        // 如果桶数组是null 或者 桶数组长度小于MIN_TREEIFY_CAPACITY 默认是64 会先进行扩容而不是树化
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 否则进行树化操作
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            HashMap.TreeNode<K,V> hd = null, tl = null;
            do {
                HashMap.TreeNode<K,V> 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);
        }
    }
1.3.4 resize 扩容方法
    final HashMap.Node<K,V>[] resize() {
        // 先初始化一些数据
        HashMap.Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            // 如果旧容量已经超过了最大容量,则设置容纳键值对个数也为最大。
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 如果扩容为原来的两倍小于最大容量,并且原容量大于了初识容量,
            // 则左移一位,扩容为原来的两倍。
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        // 如果原容量比0小,但是容纳的键值对不是,则容量改为原来的容纳键值对个数
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        // 否则,初始化为默认容量。
        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"})
        // 分配新的桶数组
        HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
        table = newTab;
        // 如果原先的桶数组不是空的,则需要将旧数据拷贝到新的桶数组中
        if (oldTab != null) {
            // 遍历键值对
            for (int j = 0; j < oldCap; ++j) {
                HashMap.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 HashMap.TreeNode)
                        ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // 否则将链表插入新的桶数组,并保证顺序
                    else { // preserve order
                        // lo 和 hi 分别为两个链表,用来保存原来一个桶中元素被拆分后的两个链表
                        HashMap.Node<K,V> loHead = null, loTail = null;
                        HashMap.Node<K,V> hiHead = null, hiTail = null;
                        HashMap.Node<K,V> next;
                        /**
                         * 检测节点的高位是否是 1
                         *      是 1 的放入 hi 链表中
                         *      是 0 的放入 lo 链表中
                         */
                        do {
                            next = e.next;
                            // 将哈希值和旧容量取与运算,
                            // 等于 0 代表原先的散列值是数组长度的偶数倍
                            // 所以扩容(2 倍)之后,只需要呆在原地
                            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);
                        // 划分完两个链表之后
                        // lo 链表呆在原来的位置
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        // hi 链表加到【当前下标 + 旧容量】的位置
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

总结流程:

  • 先分配新的桶数组长度和新的容纳键值对个数阈值。正常扩容为两倍。
  • 创建一个新的桶数组。
  • 将原键值对拷贝到新的哈希表中,遍历桶数组所有项:
    • 如果是单个节点,则直接重新计算下标放入新桶数组。
    • 如果是红黑树,则将红黑树拆分为链表,再在必要的时候在树化。
    • 如果是链表,则定义两个链表变量,遍历原链表,将hash值与旧长度取与运算,如果等于0,则说明hash值,是原长度的偶数倍,则方在新桶数组的当前下标位置。如果等于1,说明hash值是原长度的奇数倍,则放在新数组的【当前下标 + 旧容量】的位置

1.4 解决哈希冲突的方法

  • 链地址法:也就是常说的拉链法,也就是 HashMap 使用的方法,将冲突的各个节点通过 next 指针,转换为一个链表。之后的获取操作,先获取对应下标,再遍历链表通过 equals 得到对应的节点。
  • 开放定址法:也就是通过散列函数得到对应下标之后,如果有冲突,则继续向后遍历,找到空闲的位置之后填入。之后的搜索操作也相同,如果对应的位置上不是想要的节点,则向后遍历。
  • 再哈希法:通过多个哈希函数进行下标的计算,第一个计算出的下标如果有冲突,则继续使用下一个哈希函数进行计算,直到放入空闲的位置。
  • 建立公共溢出区:也就是分为一个基础的哈希表和一个溢出区,如果发生哈希冲突则将对应的节点加入到溢出区。

2. ConcurrentHashMap

ConcurrentHashMap 是线程安全的HashMap,又不像 HashTable 一样都通过加互斥锁来同步造成效率低下。

2.1 JDK 7 ConcurrentHashMap

在这里插入图片描述

  • JDK1.7 ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。
  • 也就是ConcurrentHashMap 管理了很多Segment ,Segment 又管理了一些HashEntry,每次到对应segment 中操作时,就进行加锁。
  • segment 数量是初始化时 concurrencyLevel 决定的,并且之后不会更改,所以在键值对数量大之后,锁粒度会加大,降低性能。
  • 并且ReentrantLock 相比于 synchronized 会更加占用空间。

2.2 JDK 8 ConcurrentHashMap

2.2.1 关键字段

一些和HashMap 一样的就不赘述了

    /**
     * 默认为 0
     * 如果为 -1 则表示正在初始化
     * 扩容时,值为 - (1 + 扩容线程数)
     * 扩容或初始化完成后,则是下一次扩容的阈值
     */
    private transient volatile int sizeCtl;

    /**
     * 表示是扩容完成的节点
     */
    static final int MOVED     = -1;

    /**
     * 表示是树节点
     */
    static final int TREEBIN   = -2;

    /**
     * fwd节点,表示已经被多线程处理过
     * @param <K>
     * @param <V>
     */
    static final class ForwardingNode<K,V> extends ConcurrentHashMap.Node<K,V>


2.2.2 主要方法
get 获取方法
    public V get(Object key) {
        ConcurrentHashMap.Node<K,V>[] tab; ConcurrentHashMap.Node<K,V> e, p; int n, eh; K ek;
        // 高十六位与第十六位异或获取hash值,并转换为正数
        int h = spread(key.hashCode());
        // 判断桶数组是否被初始化
        if ((tab = table) != null && (n = tab.length) > 0 &&
                // 原子性的获取桶数组位于该下标的节点e
                (e = tabAt(tab, (n - 1) & h)) != null) {
            // 如果头结点就是需要找的key,则直接返回
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 如果头结点e的hash值小于0,则说明正在扩容或者是树节点,则调用另外的find方法
            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;
    }


    /**
     * 高位和地位异或获取hash值,使得高低位都能参与hash减小hash冲突。
     * & HASH_BITS(0x7fffffff)来保证返回值为正数
     * @param h
     * @return
     */
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

    /**
     * 该方法获取对应桶数组下标链表的头结点
     * @param tab
     * @param i
     * @param <K>
     * @param <V>
     * @return
     */
    static final <K,V> ConcurrentHashMap.Node<K,V> tabAt(ConcurrentHashMap.Node<K,V>[] tab, int i) {
        // 通过 unsafe 类的 getObjectVolatile 方法原子性的获取。
        return (ConcurrentHashMap.Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

流程总结:

  • get 操作全程不加锁。
  • 通过 UnSafe 的 getObjectVolatile 方法原子性地获取对应桶下标的头结点。
put 添加方法
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /**
     * 真正执行的添加方法
     * @param key
     * @param value
     * @param onlyIfAbsent 该参数为 true 则表示,只有第一次添加该键值对会加入到 map
     * @return
     */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        // 同样获取hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        // 自旋操作
        for (ConcurrentHashMap.Node<K,V>[] tab = table;;) {
            // 定义一些遍历,f 是头结点;i 是下标;fh 是头节点的hash
            ConcurrentHashMap.Node<K,V> f; int n, i, fh;
            // 如果桶数组还没有初始化,则初始化桶数组(懒加载)
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 如果桶数组该下标为null,则通过 CAS 创建头结点,成功则break,失败就重新循环重试
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                        new ConcurrentHashMap.Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果发现头结点hash 等于 MOVED 也就是 -1,则说明在扩容阶段
            // 会帮忙进行扩容操作
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            // 如果上面都没有进入,则进入该过程也就是会加入到该链表(红黑树)中的某个位置
            else {
                V oldVal = null;
                // 给该链表的头结点加上互斥锁
                synchronized (f) {
                    // 再次判断该位置的下标的头结点是不是f,也就是有没有锁错
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            // 自旋遍历链表
                            for (ConcurrentHashMap.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;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                ConcurrentHashMap.Node<K,V> pred = e;
                                // 没有找到则在尾部插入
                                if ((e = e.next) == null) {
                                    pred.next = new ConcurrentHashMap.Node<K,V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        // 如果是红黑树节点,则在红黑树中加入节点
                        else if (f instanceof ConcurrentHashMap.TreeBin) {
                            ConcurrentHashMap.Node<K,V> p;
                            binCount = 2;
                            if ((p = ((ConcurrentHashMap.TreeBin<K,V>)f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    // 如果bin数量超过树化大小,则树化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 更新键值对个数
        addCount(1L, binCount);
        return null;
    }


    /**
     * 初始化桶数组
     * @return
     */
    private final ConcurrentHashMap.Node<K,V>[] initTable() {
        ConcurrentHashMap.Node<K,V>[] tab; int sc;
        // 没有初始化成功就自旋
        while ((tab = table) == null || tab.length == 0) {
            // 如果 sizeCtl 的值小于 0,则表示有线程在进行初始化。
            if ((sc = sizeCtl) < 0)
                // yield 让出线程
                Thread.yield(); // lost initialization race; just spin
            // 尝试 CAS 将 sizeCtl 设置为-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")
                        ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

流程总结:

  • 还是通过Unsafe原子性的获取对应下标的头结点。
  • 如果不存在,直接通过CAS插入,如果在扩容中,则帮助扩容。
  • 如果是有一条链表(红黑树)则锁住该链表(红黑树)进行插入。
  • 并最终添加键值对个数
addCount 递增键值对个数方法
    /**
     * 增加键值对个数的方法
     * 该方法类似于 LongAdder 的累加方法,通过一个Cells数组,再不同的单元中累加再求和
     * @param x
     * @param check
     */
    private final void addCount(long x, int check) {
        ConcurrentHashMap.CounterCell[] as; long b, s;
        // 如果 cells 数组已经创建,
        // 或者 如果没有创建直接通过 CAS 递增失败了,说明有线程竞争
        if ((as = counterCells) != null ||
                !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            ConcurrentHashMap.CounterCell a; long v; int m;
            // true 表示有线程竞争
            boolean uncontended = true;
            // 如果数组还没有创建
            // 或者该线程位置的数组项没有创建
            // 或者通过CAS在该数组项添加失败
            if (as == null || (m = as.length - 1) < 0 ||
                    (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                    !(uncontended =
                            U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                // 则进入该方法,创建 cells 数组,或者自旋 CAS 递增
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            // 遍历 cells 数组统计和。
            s = sumCount();
        }
        // 表示一定是一个 put 操作。
        if (check >= 0) {
            ConcurrentHashMap.Node<K,V>[] tab, nt; int n, sc;
            // 长度大于阈值需要扩容则,进入循环
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                    (n = tab.length) < MAXIMUM_CAPACITY) {
                // 拿到扩容戳,扩容戳一样的线程才能一起扩容
                int rs = resizeStamp(n);
                if (sc < 0) {
                    // 因为一些条件不需要再扩容了,直接break
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0)
                        break;
                    // 将 sc + 1 成功,说明允许帮助扩容。
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                // 说明是扩容的第一个线程,则开始扩容
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                        (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

transfer扩容方法
    /**
     * 扩容操作
     * @param tab
     * @param nextTab
     */
    private final void transfer(ConcurrentHashMap.Node<K,V>[] tab, ConcurrentHashMap.Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // 如果是协助的线程,分配一段桶进行扩容
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        // 如果是进行第一个扩容的线程
        if (nextTab == null) {            // initiating
            try {
                // 创建的新桶数组是原来的两倍。
                @SuppressWarnings("unchecked")
                ConcurrentHashMap.Node<K,V>[] nt = (ConcurrentHashMap.Node<K,V>[])new ConcurrentHashMap.Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            // 表示扩容到什么位置的下标
            transferIndex = n;
        }
        int nextn = nextTab.length;
        ConcurrentHashMap.ForwardingNode<K,V> fwd = new ConcurrentHashMap.ForwardingNode<K,V>(nextTab);
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            ConcurrentHashMap.Node<K,V> f; int fh;
            // 循环处理节点
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                else if (U.compareAndSwapInt
                        (this, TRANSFERINDEX, nextIndex,
                                nextBound = (nextIndex > stride ?
                                        nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 如果该位置没有节点,则设置为fwd节点,已经处理过
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // 如果是MOVED节点,则说明以及扩容过。
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 如果头结点有数据,则锁住该链表,进行扩容
                synchronized (f) {
                    // 扩容的具体操作和HashMap类似,运用两个链表分离。
                    if (tabAt(tab, i) == f) {
                        ConcurrentHashMap.Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & n;
                            ConcurrentHashMap.Node<K,V> lastRun = f;
                            for (ConcurrentHashMap.Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (ConcurrentHashMap.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 ConcurrentHashMap.Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new ConcurrentHashMap.Node<K,V>(ph, pk, pv, hn);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                        else if (f instanceof ConcurrentHashMap.TreeBin) {

                            /**
                             * 如果是红黑树节点则拆分
                             */
                            . . .
                        }
                    }
                }
            }
        }
    }

3. LinkedHashMap

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

LinkedHashMap 继承自 HashMap,是 HashMap 的一个扩展,所以大体的字段结构都与HashMap 类似,只是在一些功能加上来自己的实现。

3.1 主要属性

大部分的属性都由 HashMap 定义,LinkedHashMap 为了完成链表的功能而加入了一些节点信息。

    /**
     * LinkedHashMap 的节点类型
     * 在 HashMap 的基础上增加来前后指针,让所有节点形成一条双向链表
     * @param <K>
     * @param <V>
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        LinkedHashMap.Entry<K,V> before, after;
        Entry(int hash, K key, V value, HashMap.Node<K,V> next) {
            super(hash, key, value, next);
        }
    }


    /**
     * 双向链表的头节点,也就是存在最久的节点
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * 双向链表的尾节点,也就是新插入的节点
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /**
     * 存取顺序,链表的排序规则
     *   true   按照访问的顺序排序,也就是每访问一次就会插入到链表的尾部;
     *   false  按照插入的顺序排序,也就是插入节点之后,位置不会发生变化,直到再次插入;
     */
    final boolean accessOrder;

3.2 主要方法

初始化方法

LinkedHashMap 的初始化就是调用父类 HashMap 的初始化,外加对于 accessOrder 属性的设置,默认为 false,也就是按照插入顺序排序。

    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

插入方法

LinkedHashMap 的插入还是使用了 HashMap 的 putVal 方法,但是在部分细节上,使用了LinkedHashMap 的具体实现;

  • 在插入节点的时候,会使用 newNode 来构造节点,LinkedHashMap 对这个方法做了自己的具体实现:创建一个自己的包含前后指针的节点,并维护链表的头尾节点的关系。也就是把新加入的节点加入到链表尾部。
// 该下表没有节点的情况
tab[i] = newNode(hash, key, value, null);
// 对应下表是链表的情况
p.next = newNode(hash, key, value, null);
    HashMap.Node<K,V> newNode(int hash, K key, V value, HashMap.Node<K,V> e) {
        // 创建一个包含指针的节点
        LinkedHashMap.Entry<K,V> p =
                new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        构造链表的关系
        linkNodeLast(p);
        return p;
    }

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            // 将新加入的节点加入到尾部
            p.before = last;
            last.after = p;
        }
    }

看了一些其他操作的源码发现,LinkedHashMap 就是继承自 HashMap 的基础上,构造了一个自己的节点类型,实现了以下三个方法,在插入获取删除的时候,对双向链表进行维护。比较简单,也不再具体分析了。

    /**
     * 读取节点操作后的维护动作
     *      也就是当 accessOrder 设置为 true 的时候,
     *      会将被访问的节点更新到链表尾部。
     * @param p
     */
    void afterNodeAccess(HashMap.Node<K,V> p) { }

    /**
     * 插入节点后的维护动作
     *      插入一个节点之后,将该节点加到链表的尾部
     * @param evict
     */
    void afterNodeInsertion(boolean evict) { }

    /**
     * 删除节点的维护动作
     *      当一个节点被删除之后,就会在链表中也将该节点删除
     * @param p
     */
    void afterNodeRemoval(HashMap.Node<K,V> p) { }

二. List

1. CopyOnWriteArrayList

CopyOnWriteArrayList 主要通过写时复制使得迭代器遍历该list的情况下,遭到其他线程的修改不会抛出ConcurrentModificationException 异常。但是修改实时性并不是那么强。

1.1 add 添加方法

  • 通过获取全局锁,锁住来保证写入的安全,并且不会在原本的数组上修改,而是拷贝一份,写入完成后再拷贝回去。
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

1.2 get 获取方法

  • get 方法直接不加锁的获取,实时性不强。
  • 但是读操作和写操作不会相互阻塞,只有写操作和写操作会相互阻塞。
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

三. 阻塞队列

1. 概述

阻塞队列是一个支持两个附加操作的队列,即,支持阻塞的插入和移除操作。

  • 阻塞的插入:指当队列满时,会阻塞忘队列插入元素的线程,直到队列不满。
  • 阻塞的移除:指当队列为空的时候,会阻塞企图删除队列元素的线程,直到队列非空。

这两者的关系正好对应了生产者消费者的场景。

对于这两种附加操作的实现,主要提供了四种处理方式:

  • 抛出异常:队列满时添加会抛出 IllegalStateException,而当队列空删除元素时,会抛出 NoSuchElementException 异常。
  • 返回特殊值:例如添加操作返回 false,或者移除操作返回 null 等。
  • 一直阻塞:队列满时,生产者线程往队列插入元素,则阻塞直到队列可用或者受到中断。同理,队列空时移除操作也会被阻塞。
  • 超时退出:在上一个阻塞的基础上增加时间限制,超时则返回。

2. Java 中的阻塞队列

Java 阻塞队列的实现主要包括以下:

在这里插入图片描述

2.1 ArrayBlockingQueue

ArrayBlockingQueue 是一个由数组实现的有界阻塞队列。

final Object[] items;
  • 以 put 方法为例,阐述阻塞队列如何保证线程安全并且确保阻塞获取。其实就是获取全局锁之后对队列进行操作,队列满则阻塞,生产者生产元素则唤醒等待的消费者。同理,消费的处理逻辑也大同小异。
    public void put(E e) throws InterruptedException {
        // 校验插入的元素
        checkNotNull(e);
        // 获取全局锁,放置到栈中的局部变量槽中,读取更快
        final ReentrantLock lock = this.lock;
        // 可打断的获取锁
        lock.lockInterruptibly();
        try {
            // 循环如果队列满则阻塞,while 防止虚假唤醒
            while (count == items.length)
                notFull.await();
            // 
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

    private void enqueue(E x) {
        // 得到数据数组的引用
        final Object[] items = this.items;
        // 添加元素
        items[putIndex] = x;
        if (++putIndex == items.length)
            putIndex = 0;
        count++;
        // 唤醒等待消费的线程
        notEmpty.signal();
    }

2.2 LinkedBlockingQueue

LinkedBlockingQueue 是一个链表构成的有界阻塞队列,主要的属性如下所示,不难看出,链表为单向链表并且保存虚拟的首尾节点,有界的界限默认为 Integer.MAX_VALUE。

另外一个有意思的现象是,该阻塞队列包含了两把锁,一把用于添加元素的时候锁定,一把用于获取元素的时候锁定。

    /**
     * 节点类型,单向链表
     * @param <E>
     */
    static class Node<E> {
        E item;
        Node<E> next;
        Node(E x) { item = x; }
    }

    /**
     * 容量,默认为 Integer.MAX_VALUE 
     * this(Integer.MAX_VALUE);
     */
    private final int capacity;

    /**
     * 链表的虚拟头节点,数据值一直为 null
     */
    transient Node<E> head;

    /**
     * 链表的尾节点,next值一直为 null
     */
    private transient Node<E> last;

    /**
     * 获取元素的全局锁
     */
    private final ReentrantLock takeLock = new ReentrantLock();
    
    /**
     * 插入元素的全局锁
     */
    private final ReentrantLock putLock = new ReentrantLock();

在获取方法 put 中 只会通过 putLock.lockInterruptibly(); 把 putLock 阻塞,而不会直接阻塞整个队列,获取锁之后就在链表末尾插入节点。

   public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();
        try {
            while (count.get() == capacity) {
                notFull.await();
            }
            enqueue(node);
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty();
    }

移除方法的时候同理,只需要获取takeLock,然后再将首元素删除即可。

2.3 PriorityBlockingQueue

PriorityBlockingQueue 是一个支持优先级的无界阻塞队列,默认采用升序排列,可以对自定义的类重写compareTo 方法来自定义排序规则或者传入 comparator 比较器。另外,该队列采用的是堆排序的方式。

PriorityBlockingQueue 将元素存储在一个数组中,默认的初始容量是11。

    private static final int DEFAULT_INITIAL_CAPACITY = 11;
    
    private transient Object[] queue;

既然是数组,而又是无界的队列,当时需要进行扩容的操作,扩容也是创建一个新的数组,在将原数组拷贝进去。

    /**
     * 扩容大小是:
     *      (1)如果原本小于 64,则扩容为原容量的两倍+2
     *      (2)如果大于等于 64,则扩容为原容量的1.5倍
     */
    int newCap = oldCap + ((oldCap < 64) ? (oldCap + 2) : (oldCap >> 1));

2.4 DelayQueue

延时队列支持延时获取元素,内部使用 PriorityQueue 来存储元素,加入的元素必须实现 Delayed 接口。创建元素的时候可以指定多久获取该元素,只有到达时间了才能获取。

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
        implements BlockingQueue<E> {

    
    private final PriorityQueue<E> q = new PriorityQueue<E>();

}

具体的实现就是在添加元素的时候指定需要延时的时间,因为使用优先队列存储元素,所以头元素就是最早到时的元素,如果时间到了直接获取,如果没到的话就通过 available.awaitNanos(delay); 方法阻塞,另外,每次只允许一个线程进行获取。

延时队列的应用有很多:

  • 缓存过期的设计,使用延时队列保存缓存元素的有效期,使用一个线程循环查询,从延时队列获取到了元素,则说明到期。
  • 定时任务调度,将定时任务和对应的时间加入到延时队列中,一个消费者不断获取任务,获取到则说明到期,则执行。
    /**
     * 延时队列的获取方法
     * @return
     * @throws InterruptedException
     */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        // 全局锁
        lock.lockInterruptibly();
        try {
            for (;;) {
                // 获取头节点
                E first = q.peek();
                if (first == null)
                    available.await();
                else {
                    // 获取延迟时间
                    long delay = first.getDelay(NANOSECONDS);
                    // 如果延迟时间到了直接获取
                    if (delay <= 0)
                        return q.poll();
                    first = null; 
                    // 如果有线程在获取元素则阻塞,也就是一次只能一个线程获取元素
                    if (leader != null)
                        available.await();
                    else {
                        // 否则将当前线程设置为 leader 线程
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            // 没到时间则阻塞对应的延时时间
                            available.awaitNanos(delay);
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            // 唤醒等待线程以及解锁。
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

2.5 SynchronousQueue

SynchronousQueue 是一个不存储元素的阻塞队列,也就是一个 put 操作必须对应一个 take 操作。

适合传递性的场景,内部没有锁,通过 CAS 来添加节点,获取节点。没有存取操作对应会一直自旋。

2.6 LinkedTransferQueue

LinkedTransferQueue 是一个由链表结构构成,单向链表,只有一个 next 指针,包含首尾节点的无界阻塞队列,它和其他阻塞队列的主要区别在于:

  • 它内部不包含锁字段,通过乐观锁 CAS 来保证线程的安全性,更轻量级。
  • 通过一个 xfer 方法,当加入一个元素时,如果有线程在阻塞获取元素,会直接将元素传递,类似于上面的 SynchronousQueue。

2.7 LinkedBlockingDeque

LinkedBlockingDeque 是一个双向的有界阻塞队列,可以在初始化的时候传入容量,因为是双向队列,所以操作更灵活,内部包含了一个全局锁来保证线程安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值