深入理解 ConcurrentHashMap

前文

哈希映射(HashMap)

深入理解 HashMap

谈谈对 ReentrantLock 的理解

并发容器 ConcurrentHashMap

JDK1.7 中的 ConcurrentHashMap

    在 JDK1.7 版本中,ConcurrentHashMap 使用了 Segment 分段锁 的设计原理,Segment 数组的意义就是将一个大的 table 分割成多个小的 table 来进行加锁,也就是锁分离技术,而每一个 Segment 元素存储的是 HashEntry 数组 + 链表,这个和 JDK1.7 中的 HashMap 一样,Segment 的数量取决于 ConcurrencyLevel(下面会讲到)
在这里插入图片描述
每个 Segment 里面可以看成一个 HashMap,每个 Segment 块都有自己独立的 ReentrantLock 锁,所以并发操作时每个 Segment 互不影响

在这里插入图片描述
在 JDK1.7 中 Segment 继承了 ReentrantLock,ReentrantLock 是一种可重入锁,可重入就是说可以支持一个线程对锁的重复获取,具体关于 ReentrantLock 的介绍可以看这篇文章 谈谈对 ReentrantLock 的理解

JDK1.8 中的 ConcurrentHashMap

在这里插入图片描述
在 JDK1.8 中的 ConcurrentHashMap 抛弃了分段锁的设计原理,而是直接用 Node 数组 + 链表 + 红黑树的数据结构来实现,并发操作采用 synchronized + CAS 来实现,整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧的版本

在这里插入图片描述
注释的翻译为:先前版本中使用的精简版帮助程序类,为实现序列化兼容性而声明

ConcurrentHashMap 中的基本属性

// 序列ID
private static final long serialVersionUID = 7249069246763182397L;

// 表最大容量:2^30 = 1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认的初始值,必须是2的整数幂
private static final int DEFAULT_CAPACITY = 16;

// 最大可能的数组大小(非2的幂),toArray和相关方法需要。
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

// 默认并发级别,未使用,但定义为与此类的先前版本兼容。
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

// 加载因子(负载因子)
private static final float LOAD_FACTOR = 0.75f;

// 链表转红黑树的阀值,如果大于这个阀值,则链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;

// 红黑树转链表的阀值,如果小于这个阀值,则红黑树转回链表
static final int UNTREEIFY_THRESHOLD = 6;

// 最小容量(如果红黑树的节点过多,则将调整表的大小),该值至少应为 4 * TREEIFY_THRESHOLD(8)
// 这样做是避免调整大小和树化阀值之间存在冲突
static final int MIN_TREEIFY_CAPACITY = 64;

// 每个传输步骤的最小重新绑定数量,该值最少为 DEFAULT_CAPACITY 也就是16
private static final int MIN_TRANSFER_STRIDE = 16;

// 用于生成标记的位数,对于32位的阵列,必须至少为6
private static int RESIZE_STAMP_BITS = 16;

// 可以帮助调整大小的最大线程数,必须为2的15次方
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

// 在sizeCtl中记录size大小的偏移量 32-16=16
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

// Node的hash值
static final int MOVED     = -1; // hash for forwarding nodes

// 树节点的hash值
static final int TREEBIN   = -2; // hash for roots of trees

// ReservationNode的hash值
static final int RESERVED  = -3; // hash for transient reservations

// 普通节点哈希的可用位
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();

// 散列表初始化和扩容的大小都是由该变量来控制
// 控制标识符:用来控制表(table)的初始化和扩容操作,不同的值有不同的含义
// 当为负数时,则表(table)正在初始化:-1用于初始化,-N代表有N-1个线程正在进行扩容操作
// 当为null时:保留要使用的初始表大小创建或默认为0
// 初始化后按下一次进行扩容的大小
private transient volatile int sizeCtl;

// 装载Node的数组,作为ConcurrentHashMap的数据容器,采用懒加载的方式
// 直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方(因为继承自HashMap)
transient volatile Node<K,V>[] table;

// 扩容时使用,平时为null,只有在扩容的时候才为非null。逻辑机制和ArrayList底层的数组扩容一致
private transient volatile Node<K,V>[] nextTable;

// 元素数量计数器,该值也是一个阶段性的值(产出的时候可能容器正在被修改),通过CAS的方式进行更改
// 关于CAS是什么,稍后会解释
private transient volatile long baseCount;

// Unsafe 类
private static final sun.misc.Unsafe U;

// 当resize的时候下一个tab下标的索引值(当前值+1)
private transient volatile int transferIndex;

// 当resize和创建counterCells的时候自选锁,和longadder一致
private transient volatile int cellsBusy;

// 和longadder的cells一致
private transient volatile CounterCell[] counterCells;

ConcurrentHashMap 的构造函数

ConcurrentHashMap()

来看下面这段代码
在这里插入图片描述
当 new 一个 ConcurrentHashMap 未指定大小的时候,这是默认的构造方法,使用默认的初始大小(16)
在这里插入图片描述

ConcurrentHashMap(int initialCapacity)

再看下下面这段代码
在这里插入图片描述
根据给定的值初始化

// 初始化时明确给定一个初始容量,减少 resize 次数
public ConcurrentHashMap(int initialCapacity) {
	// 如果给定的初始值小于 0
    if (initialCapacity < 0)
    	// 则抛出异常
        throw new IllegalArgumentException();
    int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
               MAXIMUM_CAPACITY :
               // tableSizeFor 方法对于给定的目标容量,返回两倍大小的幂
               tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
    this.sizeCtl = cap;
}

上面代码中提到的 tableSizeFor 方法跟 HashMap 中的 tableSizeFor 方法是一样的,不懂的朋友可以看这篇文章,tableSizeFor 方法详细介绍

ConcurrentHashMap(Map<? extends K, ? extends V> m)

看下下面这段代码
在这里插入图片描述
创建一个与给定的Map映射具有相同元素的 Map,默认是构建一个 容量为 16 的 ConcurrentHashMap
在这里插入图片描述

ConcurrentHashMap 中的 Node

Node 是 ConcurrentHashMap 存储结构的基本单元,继承了 Map 中的 Entry,用于存储数据,Node 数据结构很简单,就是一个链表,但是只允许对数据进行存储和查找,不允许进行修改,源码如下:

static class Node<K,V> implements Map.Entry<K,V> {
    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;
    }
}

ConcurrentHashMap 中的 TreeNode

TreeNode 继承自 Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的长度大于 8 时会转换成红黑树的结构,通过 TreeNode 作为存储结构代替 Node 来转换成红黑树源代码如下:

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;
    }
}

ConcurrentHashMap 中的 TreeBin

TreeBin 从字面含义中可以理解为存储树型结构的容器,而树型结构就是指 TreeNode,所以 TreeBin 就是封装 TreeNode 的容器,TreeBin 提供转换红黑树的一些条件和锁的控制
在这里插入图片描述

put()

来看下 ConcurrentHashMap 的 put 操作
在这里插入图片描述
在这里插入图片描述
在 put() 方法中返回了 putVal() 方法,继续看下 putVal() 方法

final V putVal(K key, V value, boolean onlyIfAbsent) {
	// 在 ConcurrentHashMap 中是不允许 key 和 value 为 null 的,否则会抛出空指针异常
    if (key == null || value == null) throw new NullPointerException();
    // 计算 hash 值
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 无限循环,类似于 while(true)
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 如果表为空或者表的长度为 0 时
        if (tab == null || (n = tab.length) == 0)
        	// 初始化表
            tab = initTable();
        // i = (n - 1) & h 等价于 i = hash % n(n 为 数组的长度,前提是 n 得为 2 的幂次方)
        // 这里取出 table 中位置的节点用 f 表示
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        	// 使用 CAS 中的 casTabAt 方法比较并交换值,如果 table 的第 i 项为 null 则用新生成的 Node 替换
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        // 判断 table[i] 节点的 hash 是否等于 MOVED(用于转发节点的哈希,值为 -1),如果等于,则检测到正在扩容
        else if ((fh = f.hash) == MOVED)
        	// 进行节点转移(也就是说在扩容的过程中)
            tab = helpTransfer(tab, f);
		// 如果 hash 冲突了,且 hash 值不为 -1,也就是不等于 MOVED 了
        else {
            V oldVal = null;
            // 同步 f 节点,防止增加链表的时候导致链表形成环状
            synchronized (f) {
            	// 避免多线程,需要重新检查,找到 table 表下标为 i 的节点
                if (tabAt(tab, i) == f) {
                	// 如果链表节点的 hash 值大于等于 0
                    if (fh >= 0) {
                    	// 初始化链表的长度,赋值为 1
                        binCount = 1;
                        // 无限循环
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 节点的 hash 值相等并且 key 也相等
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                // 保存该节点的 val 值
                                oldVal = e.val;
                                // 判断
                                if (!onlyIfAbsent)
                                	// 将指定的 value 保存至节点,即进行节点值的更新
                                    e.val = value;
                                // 退出循环
                                break;
                            }
                            // 保存当前节点
                            Node<K,V> pred = e;
                            // 插入到链表末尾(简单地说就是当前节点的下一个节点为空,即为最后一个节点)
                            if ((e = e.next) == null) {
                            	// new 一个节点
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                // 退出循环
                                break;
                            }
                        }
                    }
                    // 否则如果是一个树节点
                    // 节点为红黑树节点类型
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        // 初始化链表的长度,赋值为 2
                        binCount = 2;
                        // 插入到树中,也就是说将 hash、key、value 放入到红黑树中
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            // 保存节点的 val
                            oldVal = p.val;
                            // 判断
                            if (!onlyIfAbsent)
                           		// 赋予节点 value 值
                                p.val = value;
                        }
                    }
                }
            }
            // 当链表长度不为 0 时
            if (binCount != 0) {
            	// 如果链表长度已经达到临界值 8,那么就将链表转换为树结构
                if (binCount >= TREEIFY_THRESHOLD)
                	// 转换为树结构
                    treeifyBin(tab, i);
                // 如果旧值不为空时
                if (oldVal != null)
                	// 返回旧值
                    return oldVal;
                // 退出循环
                break;
            }
        }
    }
   	// 将当前ConcurrentHashMap 的 size 加一
    addCount(1L, binCount);
    return null;
}

get()

使用 get 获取 value 的时候
在这里插入图片描述

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 计算 hash 值
    int h = spread(key.hashCode());
    // 判断数组是否为空,通过 key 定位到数组下标是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 判断 node 节点第一个元素是不是要找到,如果是直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 如果是红黑树结构,就从红黑树里面查询
        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;
}

总结:

从上面的代码可以看出在 JDK1.8 中的 ConcurrentHashMap 与 HashMap 非常相似,只是 ConcurrentHashMap 中增加了同步的操作和 CAS 来实现并发操作

  • 在 JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,将一个 HashMap 切割成 Segment 数组,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全
  • 在 JDK1.8 中 ConcurrentHashMap 引入了红黑树,由于 JDK 1.7 中的 ConcurrentHashMap 是基于链表实现的,所以遍历是一个很漫长的过程(时间复杂度为 O(n)),而红黑树的遍历效率是很快的(时间复杂度为 O(logn)),代替一定的阀值的链表
  • 在 JDK1.8 中,与此对应的 ConcurrentHashMap 也是采用了与 HashMap 类似的存储结构,但是 JDK1.8 中 ConcurrentHashMap 并没有再采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性,源码的实现比 JDK1.7 要复杂的多
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值