JUC系列并发容器之ConcurrentHashMap(JDK1.8版)

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC系列并发容器之ConcurrentHashMap(JDK1.8版)相关的知识,希望对你有一定的参考价值。

ConcurrentHashMap(JDK1.8)

文章目录


在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap。在使用中发现最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。

组成

内部类

Node

拥有键值对的链表的基础存储结构

    static class Node<K,V> implements Map.Entry<K,V> 
        // key的hash值
        final int hash;
        // 关键字
        final K key;
        // 存储值
        volatile V val;
        // 下一个结点
        volatile Node<K,V> next;

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

        public final K getKey()        return key; 
        public final V getValue()      return val; 
        public final int hashCode()    return key.hashCode() ^ val.hashCode(); 
        public final String toString() return key + "=" + val; 
        public final V setValue(V value) 
            throw new UnsupportedOperationException();
        

        public final boolean equals(Object o) 
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         */
        Node<K,V> find(int h, Object k) 
            Node<K,V> e = this;
            if (k != null) 
                do 
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                 while ((e = e.next) != null);
            
            return null;
        
    

ForwardingNode

在transfer操作期间插入到 bin 头部的节点。

static final class ForwardingNode<K,V> extends Node<K,V> 
    // node数组,存放的什么?
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) 
        super(MOVED, null, null, null);
        this.nextTable = tab;
    

    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;
            if (k == null || tab == null || (n = tab.length) == 0 ||
                (e = tabAt(tab, (n - 1) & h)) == null)
                return null;
            for (;;) 
                int eh; K ek;
                if ((eh = e.hash) == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                if (eh < 0) 
                    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;
            
        
    

TreeNode

作为红黑树结构的存储结构,比一般红黑树存储结构出来的next和prev,可以将这些结点变成双向的链表结构,是为了方便从链表变为红黑树,在从红黑树变成链表。在ConcurrentHashMap中,链表与红黑树的转变是依据链表中的结点数量,默认变成红黑树的链表结点个数需要大于8。

hashkeyvalnextprevparentleftrightred
key的hash值关键字具体值下一个结点上一个结点双亲结点左子结点右子结点结点颜色
static final class TreeNode<K,V> extends Node<K,V> 
    TreeNode<K,V> parent;  // red-black tree links
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // needed to unlink next upon deletion
    boolean red;

    TreeNode(int hash, K key, V val, Node<K,V> next,
             TreeNode<K,V> parent) 
        super(hash, key, val, next);
        this.parent = parent;
    

    Node<K,V> find(int h, Object k) 
        return findTreeNode(h, k, null);
    

    /**
     * Returns the TreeNode (or null if not found) for the given key
     * starting at given root.
     */
    final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) 
        if (k != null) 
            TreeNode<K,V> p = this;
            do  
                int ph, dir; K pk; TreeNode<K,V> q;
                TreeNode<K,V> pl = p.left, pr = p.right;
                if ((ph = p.hash) > h)
                    p = pl;
                else if (ph < h)
                    p = pr;
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                else if (pl == null)
                    p = pr;
                else if (pr == null)
                    p = pl;
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                else if ((q = pr.findTreeNode(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
             while (p != null);
        
        return null;
    

TreeBin

继承Node,树标记结构,表明该结点背后有一棵红黑树。在新增结点和删除结点的时候,为了并发的安全都需要会进行锁的竞争。root和first不应该是同一个结点。

        TreeNode<K,V> root;
        volatile TreeNode<K,V> first;
        volatile Thread waiter;
        volatile int lockState;

为了减少本篇篇幅长度,将红黑树的实现部分拆分出去:ConcurrentHashMap(JDK1.8)中红黑树的实现


数据结构

核心方法

方法名描述
V putVal(K key, V value, boolean onlyIfAbsent)将指定键映射到此表中的指定值。 键和值都不能为空。
可以通过使用与原始键相同的键调用 get 方法来检索该值。
onlyIfAbsent:true 代表 不更新旧值,false更新旧值
Node<K,V>[] initTable() 使用 sizeCtl 中记录的大小初始化表。第一级结点存储的数组结构
void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)将每个 bin 中的节点移动和/或复制到新表。
void treeifyBin(Node<K,V>[] tab, int index)在给定的索引中替换所有链接节点将简单的链表改为红黑树,除非表太小(数组长度小于64),在这种情况下对Node数组进行扩容,扩2倍
void tryPresize(int size)尝试预先调整表格大小以容纳给定数量的元素。即将Node数组扩容到指定的size大小。
void addCount(long x, int check) 添加到计数中,如果Node数组太小并且尚未调整大小,则启动transfer。 如果已经调整大小,则在工作可用时帮助执行扩容(多线程扩容,在线程数量允许的情况下,一起进行扩容操作,扩容线程已满的话,自旋重新检查)。 转移后重新检查占用情况,以查看是否已经需要再次调整大小,因为调整大小滞后于添加。
V get(Object key)返回指定键映射到的值,如果此映射不包含该键的映射,则返回 null。
更正式地说,如果此映射包含从键 k 到值 v 的映射,使得 key.equals( k),则此方法返回 v; 否则返回null。 (最多可以有一个这样的映射。)
V replaceNode(Object key, V value, Object cv)四种公共删除/替换方法的实现:用 v 替换节点值,如果非空,则以 cv 匹配为条件。 如果value为空,则删除。

int spread(int h)计算hash值

static final int HASH_BITS = 0x7fffffff; // 正常节点哈希的可用位,十六进制表示法,一个十六进制数占4个bit。int一个32位(bit),那需要8个十六进制数,故可以理解为0x7 f f f f f f f,F的二进制为1111,7的二进制0111.

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

构造函数

  1. ConcurrentHashMap(int initialCapacity)

创建一个新的空映射,其初始表大小可容纳指定数量的元素,无需动态调整大小。

参数

  • initialCapacity:初始容量。
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;

  1. ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

根据给定的元素数 (initialCapacity)、表密度 (loadFactor) 和并发更新线程数 (concurrencyLevel) 创建一个新的空映射,其初始表大小。

参数

  • initialCapacity:初始容量。 给定指定的负载因子,实现执行内部大小调整以适应这么多元素。
  • loadFactor:用于建立初始表大小的负载因子(表密度)。
  • concurrencyLevel:估计的并发更新线程数。 实现可以使用这个值作为大小提示。
    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;
    

int tableSizeFor(int c)计算数组的大小,通过初始值*1.5+1,然后向上取2的n次方。

    private static final int tableSizeFor(int c) 
        int n = c - 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;
    

PUT过程

putVal(K key, V value, boolean onlyIfAbsent)

    public V put(K key, V value) 
        return putVal(key, value, false);
    

	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;
        // 存放数据的Node<K,V>数组
        for (Node<K,V>[] tab = table;;) 
            Node<K,V> f; int n, i, fh;
            // 若Node<K,V>[]还是null或没有元素,则需要进行初始化
            if (tab == null || (n = tab.length) == 0)
                // 初始化数组
                tab = initTable();
            // 至此代表容器中的数组已经成功初始化,找该 hash 值对应的数组下标,得到第一个节点 f;
            // 若f为null,使用CAS操作直接将新节点放到数组下标i的位置,执行成功直接退出for,执行失败代表存在竞争,继续执行
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) 
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            
            // MOVED代表在扩容
            else if ((fh = f.hash) == MOVED)
                // 帮助数据迁移
                tab = helpTransfer(tab, f);
            else 
                // 此时数组当前下标的元素的头结点f,不为空
                V oldVal = null;
                // 对这个位置的头结点上锁
                synchronized (f) 
                    if (tabAt(tab, i) == f) 
                        // 对此头结点的hash进行判断,若值>=0则说明是数组此下标的元素是个链表
                        if (fh >= 0) 
                            // 链表节点计数,由头结点从1开始
                            binCount = 1;
                            // 遍历链表
                            for (Node<K以上是关于JUC系列并发容器之ConcurrentHashMap(JDK1.8版)的主要内容,如果未能解决你的问题,请参考以下文章

JUC系列并发容器之ConcurrentLinkedQueue(JDK1.8版)

JUC系列并发容器之ConcurrentHashMap(JDK1.8版)

JUC系列并发容器之ConcurrentHashMap(JDK1.8版)扩容图解说明

Java并发编程系列之三JUC概述

JUC系列01之大话并发

Java并发编程系列之三JUC概述