目录

  • `Hashtable` 与 `HashMap` 的比较
  • `ConcurrentHashMap` 概述
    • `jdk1.7` 版本中
    • `jdk1.8` 版本中
  • `ConcurrentHashMap` 的原理概览
  • `ConcurrentHashMap` 源码
    • `ConcurrentHashMap` 的基本属性
    • `ConcurrentHashMap` 的数据存储结构
      • 数组
      • 链表
      • 红黑树
      • 封装红黑树容器
    • `ConcurrentHashMap` 的 `put` 方法
      • `ConcurrentHashMap` 的 `put` 元素流程
    • `ConcurrentHashMap` 的 `get()` 方法
      • `ConcurrentHashMap` 的 `get()` 方法流程
    • `ConcurrentHashMap` 的 `remove()` 方法
      • `ConcurrentHashMap` 的 `remove()` 方法步骤
    • `ConcurrentHashMap` 的扩容
  • 为什么用 `synchronized` 来代替 `ReentrantLock`
  • `ConcurrentHashMap` 总结

HashtableHashMap 的比较

  • 底层的数据结构:Hashtable 是数组 + 链表,而 HashMap 是数组 + 链表 + 红黑树
  • 默认的初始容量:Hashtable11,而 HashMap16
  • 扩容大小:Hashtable 扩容后的大小为原来的 2+ 1,而 HashMap 是原来大小的 2
  • 数组的懒加载:Hashtable 在初始化时就创建了数组,HashMap 对底层数组采取的懒加载,即当执行第一次插入时才会创建数组
  • 键和值是否允许为 nullHashtable 不允许,HashMap 中键和值均允许为 null
  • 线程安全:Hashtable 是线程安全的,而 HashMap 是线程不安全的

关于 HashMap 为什么是线程不安全的,可以查看:https://blog.csdn.net/weixin_38192427/article/details/108478615

查看 Hashtable 的源码,可以看到 Hashtable 处理线程安全问题过于简单粗暴,是将所有的方法都加上了 synchronized 关键字,在竞争激烈的并发场景中性能就会非常差。鉴于这个问题在 jdk 1.5 时,增加了 ConcurrentHashMap 这个类,在并发情况下保证了线程安全,同时提供了更高的并发效率

ConcurrentHashMap 概述

  • ConcurrentHashMap 是线程安全的
  • ConcurrentHashMap 不允许为 nullkey 或者 value
  • 底层的数据结构:数组 + 单链表 + 红黑树
  • 默认的初始容量:和 HashMap 相同都是 16
  • 创建数组时机:当执行第一次插入时才会创建数组

jdk1.7 版本中

使用了分段锁技术:容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率

这就是 ConcurrentHashMap 所采用的分段锁思想,见下图:

  • 主要使用的是 Segment 分段锁
  • 内部拥有一个 Entry 数组,每个数组的每个元素又有一个链表
  • 同时 Segment 继承 ReetrantLock(独占锁或互斥锁) 来进行加锁
  • 默认 Segment16 个,也就是说可以支持 16 个线程的并发,在初始化是可以进行设置,一旦初始化就无法修改(Segment 不可扩容),但是 Segment 内部的 Entry 数组是可扩容的

jdk1.8 版本中

  • 摒弃了分段锁的概念,使用 CAS + Synchronized + volatile 代替 Segment
  • 对于锁的粒度,调整为对每个数组元素加锁
  • 如果没有 hash 冲突,就直接 CAS 插入
  • 如果产生 hash 冲突时,先使用 synchronized 加锁,在锁的内部处理 hash 冲突与 HashMap 是相同的,即使用 keyequals() 方法进行比较
  • 内部的数据结构和 HashMap 相同,使用数组 + 链表 + 红黑树

ConcurrentHashMap 的原理概览

ConcurrentHashMap 中通过一个 Node<K,V>[] 数组来保存添加到 map 中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在第一次添加元素的时候才会初始化,否则只是初始化一个 ConcurrentHashMap 对象的话,只是设定了一个 sizeCtl 变量,这个变量用来判断对象的一些状态和是否需要扩容

  • 第一次添加元素的时候,默认初始长度为 16,当往 map 中继续添加元素的时候,通过 hash 值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了 8 个以上,如果数组的长度还小于 64 的时候,则会扩容数组。如果数组的长度大于等于 64 了的话,在会将该节点的链表转换成树
  • 通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新的数组中,同一个链表中的元素通过 hash 值的数组长度位来区分,是还是放在原来的位置还是放到扩容的长度的相同位置去。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于 6 个了,则会将该树转为链表
  • 取元素的时候,相对来说比较简单,通过计算 hash 值来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断 keykeyhash 值,取出 value

ConcurrentHashMap 源码

ConcurrentHashMap 的基本属性

注意下面几个属性使用了 volatile 修饰,保证了元素在并发情况下的可见性

// node 数组最大容量:2^30=1073741824
private static final int MAXIMUM_CAPACITY = 1 << 30;// 默认初始容量,也可以指定,必须是2的幂次方
private static final int DEFAULT_CAPACITY = 16;// 并发级别,这是JDK1.7遗留下来的,为兼容以前的版本
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;// 加载因子,默认0.75
private static final float LOAD_FACTOR = 0.75f;// 链表红黑树的阈值,当存储数据之后,当链表长度大于 8 时,则将链表转换成红黑树
static final int TREEIFY_THRESHOLD = 8;/*** 红黑树还原为链表的阈值,当在扩容时,resize()方法的split()方法中使用到该字段* 在重新计算红黑树的节点存储位置后,当拆分成的红黑树链表内节点数量 小于等于6 时,则将红黑树节点链表转换成普通节点链表。* <p>* 该字段仅仅在split()方法中使用到,在真正的remove删除节点的方法中时没有用到的,实际上在remove方法中,* 判断是否需要还原为普通链表的个数不是固定为6的,即有可能即使节点数量小于6个,也不会转换为链表,因此不能使用该变量!*/
static final int UNTREEIFY_THRESHOLD = 6;// 链表红黑树的阈值,即当哈希表中的容量大于等于 64 时,才允许树形化链表,否则不进行树形化,而是扩容
static final int MIN_TREEIFY_CAPACITY = 64;// 用在transfer方法中,transfer可以并发,每个CPU(线程)所需要处理的连续的桶的个数,最少16
private static final int MIN_TRANSFER_STRIDE = 16;/*** 用于辅助生成扩容版本唯一标记,最小是6。这里是一个非final的变量,但是也没有提供修改的方法* 每次扩容都会有一个唯一的标记,一次扩容完毕之后,才会进行下一次扩容*/
private static int RESIZE_STAMP_BITS = 16;// 扩容的最大线程数, 2^15-1
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;// 扩容版本标记移位之后会保存到sizeCtl中当作扩容线程的基数,然后在反向移位可以获取到扩容版本标记
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;// ForwardingNode的hash值,一种临时节点,用于扩容时辅助扩容,相当于标志节点,不存储数据
static final int MOVED = -1;/*** TreeBin结点的hash值,用于代理红黑树根节点,会存储数据* 红黑树添加删除节点时,树结构可能发生改变,因此额外维护了一个读写锁*/
static final int TREEBIN = -2;
/*** ReservationNode的hash值,也相当于标志节点,不存储数据* 也是相当于占位符,在JDK1.8才出现的新属性,用于computeIfAbsent、compute方法,一般用不到*/
static final int RESERVED = -3;// 可用CPU数量
static final int NCPU = Runtime.getRuntime().availableProcessors();// 存放 node 的数组
transient volatile Node<K, V>[] table;// 扩容后的新的table数组,只有在扩容时才会用到(才会非null)
private transient volatile ConcurrentHashMap.Node<K, V>[] nextTable;/*** JDK1.8的新属性* 控制标识符,用来控制table的出于初始化、扩容等操作,不同的值有不同的含义:* 当为0时:代表当时的table还没有被初始化* 当为负数时:*      -1代表线程正在初始化哈希表;*      其他负数,表示正在进行扩容操作,此时sizeCtl=(rs << RESIZE_STAMP_SHIFT )+ n + 1,即此时的sizeCtl由 版本号rs左移16位 + 并发扩容的线程数n +1 组成,并不是由所谓的-(n+1)简单组成!* 当为正数时:表示初始化容量或者下一次进行扩容的阈值,即如果hash表的实际大小>=sizeCtl,则进行扩容,阈值是当前ConcurrentHashMap容量的0.75倍,不能改变*/
private transient volatile int sizeCtl;// CAS的标志位。在初始化或者counterCells数组扩容的时候会用到
private transient volatile int cellsBusy;// 元素个数基本计数器,只会记录CAS更新成功的数值,可能不准确
private transient volatile long baseCount;/*** 添加/删除元素时如果如果使用baseCountCAS计算失败* 那么使用CounterCell[]数组保存CAS失败的个数* 最后size()方法统计出来的大小是baseCount和counterCells数组的总和*/
private transient volatile CounterCell[] counterCells;/*** transfer方法用于扩容或者协助扩容,允许多个线程同时操作,但是为了防止重复操作,ConcurrentHashMap将数组一段连续的桶位分给一条线程进行操作* 下一条线程进来帮助扩容的时候需要知道上一条线程是操作了哪些桶位,这里的transferIndex就是记录了下一个将要执行transfer任务的线程的起始数组下标索引+1* transfer分配桶位的方式是从最后的索引向前分配,直到0索引位置,每次一条新线程分配了桶位,transferIndex都需要更新,* 因此如果一条线程想要帮助扩容那么需要判断transferIndex <= 0,如果成立,那么表示所有的桶位都被分配完了,不需要新来的线程帮助了*/
private transient volatile int transferIndex;

ConcurrentHashMap 的数据存储结构

数组

// 存放 node 的数组
transient volatile Node<K, V>[] table;

链表

static class Node<K,V> implements Map.Entry<K,V> {final int hash;    final K key;      // val 和 next 都会在扩容时发生变化,所以加上 volatile 来保持可见性和禁止重排序volatile V val; // get 操作全程不需要加锁是因为 Node 的成员 val 是用volatile 修饰volatile Node<K,V> next; // 表示链表中的下一个节点,数组用volatile修饰主要是保证在数组扩容的时候保证可见性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; }// 不允许更新value 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)));}// 用于map中的get()方法,子类重写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;}
}

红黑树

static final class TreeNode<K,V> extends Node<K,V> {TreeNode<K,V> parent;  // 父节点TreeNode<K,V> left; // 左子树TreeNode<K,V> right; // 右子树TreeNode<K,V> prev; // 删除时需要取消下一个链接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);}// 根据 key 查找 从根节点开始找出相应的 TreeNodefinal 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;elsep = pl;} while (p != null);}return null;}
}

封装红黑树容器

static final class TreeBin<K,V> extends Node<K,V> {TreeNode<K,V> root; // 指向 TreeNod e列表的根节点volatile TreeNode<K,V> first;volatile Thread waiter;volatile int lockState;static final int WRITER = 1; // 获取写锁的状态static final int WAITER = 2; // 等待写锁的状态static final int READER = 4; // 增加数据时读锁的状态// 初始化红黑树TreeBin(TreeNode<K,V> b) {super(TREEBIN, null, null, null);this.first = b;TreeNode<K,V> r = null;for (TreeNode<K,V> x = b, next; x != null; x = next) {next = (TreeNode<K,V>)x.next;x.left = x.right = null;if (r == null) {x.parent = null;x.red = false;r = x;}else {K k = x.key;int h = x.hash;Class<?> kc = null;for (TreeNode<K,V> p = r;;) {int dir, ph;K pk = p.key;if ((ph = p.hash) > h)dir = -1;else if (ph < h)dir = 1;else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0)dir = tieBreakOrder(k, pk);TreeNode<K,V> xp = p;if ((p = (dir <= 0) ? p.left : p.right) == null) {x.parent = xp;if (dir <= 0)xp.left = x;elsexp.right = x;r = balanceInsertion(r, x);break;}}}}this.root = r;assert checkInvariants(root);}......
}

ConcurrentHashMapput 方法

public V put(K key, V value) {return putVal(key, value, false);
}final V putVal(K key, V value, boolean onlyIfAbsent) {// K,V 都不能为空if (key == null || value == null) throw new NullPointerException();// 取得 key 的 hash 值int hash = spread(key.hashCode());// 用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树int binCount = 0;// 数组的遍历,自旋插入结点,直到成功for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh;// ----------------第一次 put 的时候 table 没有初始化,则初始化 table----------------if (tab == null || (n = tab.length) == 0)              tab = initTable();// 通过哈希计算出一个表中的位置else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// ----------------如果这个位置没有元素的话,则通过 cas 的方式尝试添加,注意这个时候是没有加锁的----------------if (casTabAt(tab, i, null, // 创建一个 Node 添加到数组中,null 表示的是下一个节点为空new Node<K,V>(hash, key, value, null)))break;         }// 如果检测到某个节点的 hash 值是 MOVED,则表示正在进行数组扩张的数据复制阶段,// 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失else if ((fh = f.hash) == MOVED)    tab = helpTransfer(tab, f);else {// ----------------此时,说明已经产生了 Hash 冲突了---------------- V oldVal = null;// ----------------synchronized----------------synchronized (f) {// 再判断一下table[i]是不是第一个结点, 防止其它线程的写修改if (tabAt(tab, i) == f) {// ----------------table[i]是链表结点----------------if (fh >= 0) {binCount = 1;            // 遍历这个链表for (Node<K,V> e = f;; ++binCount) {K ek;// 要存的元素的 hash 值和 key 跟要存储的位置的节点的相同的时候,替换掉该节点的 value 即可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;// 如果不是同样的 hash,同样的 key 的时候,则判断该节点的下一个节点是否为空if ((e = e.next) == null) { // ----------------“尾插法”插入新结点----------------pred.next = new Node<K,V>(hash, key,value, null);break;}}}// ----------------table[i]是红黑树结点----------------else if (f instanceof TreeBin) { Node<K,V> p;binCount = 2;// 调用putTreeVal方法,将该元素添加到树中去if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}if (binCount != 0) {// 当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为treeif (binCount >= TREEIFY_THRESHOLD)// 链表 -> 红黑树 转换treeifyBin(tab, i);    // 表明本次put操作只是替换了旧值,不用更改计数值   if (oldVal != null)return oldVal;break;}}}addCount(1L, binCount);// 计数值加1return null;
}

ConcurrentHashMapput 元素流程

  • 如果数组没有初始化就先调用 initTable() 方法来进行初始化
  • 在遍历该数组时,如果没有 hash 冲突就将元素使用 CAS 插入到该数组中
  • 如果还在进行扩容操作就先进行扩容
  • 否则就是存在 hash 冲突,那么就使用 synchronized 加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入。此处处理 hash 冲突和 HashMap 是相同的
  • 如果链表长度大于阈值 8 时,并且数组的长度大于 64 时,就要先转换成黑红树的结构,break 再一次进入循环
  • 如果添加成功就调用 addCount() 方法统计 sizeCtl,并且检查是否需要扩容

ConcurrentHashMapget() 方法

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());// table不能为null,且table[i]不能为空if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {// 检查头结点,table[i]就是待查找的项,直接返回if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}// 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;
}

ConcurrentHashMapget() 方法流程

get() 方法的逻辑很简单,首先根据 keyhash 值计算映射到 table 的哪个桶——table[i]

  • 如果 table[i]key 和待查找 key 相同,那直接返回;
  • 如果 table[i] 对应的结点是特殊结点(hash 值小于 0),则通过 find() 方法查找;
  • 如果 table[i] 对应的结点是普通链表结点,则按链表方式查找

ConcurrentHashMapremove() 方法

public V remove(Object key) {return replaceNode(key, null, null);
}final V replaceNode(Object key, V value, Object cv) {int hash = spread(key.hashCode());// 计算hash值for (Node<K,V>[] tab = table;;) {// 死循环,直到找到Node<K,V> f; int n, i, fh;// --------------如果table尚未初始化或命中桶位中为null,则break--------------if (tab == null || (n = tab.length) == 0 ||(f = tabAt(tab, i = (n - 1) & hash)) == null)break;// 如果检测到其它线程正在扩容,则先帮助扩容,然后再来寻找   else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);else {V oldVal = null;boolean validated = false;// 校验标记// --------------synchronized--------------// --------------开始锁住这个桶,然后比对寻找要删除的节点--------------synchronized (f) {  // 再次检查,避免由于多线程的原因table[i]已经被修改if (tabAt(tab, i) == f) { // --------------说明是链表节点--------------if (fh >= 0) {validated = true;// e表示当前循环处理的元素,pred表示当前循环节点的上一个节点for (Node<K,V> e = f, pred = null;;) {K ek;// 满足条件就是找到key出现的节点位置if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {// 当前节点的valueV ev = e.val;// --------------cv == null为true,那么就是一个删除操作,否则另外就是替换操作--------------if (cv == null || cv == ev ||(ev != null && cv.equals(ev))) {// 将当前节点的值赋值给oldVal后续返回会用到oldVal = ev;// 如果条件成立,说明当前是一个替换操作,直接替换if (value != null)e.val = value;// --------------否则就是删除操作,则删除此节点--------------else if (pred != null)// 当前节点的上一个节点,指向当前节点的下一个节点pred.next = e.next;// 说明当前节点即为头结点,只需要将桶位设置为头结点的下一个节点    elsesetTabAt(tab, i, e.next);}break;}// 更改指向,继续向后循环pred = e;if ((e = e.next) == null)// 如果为到链表末尾了,则直接退出break;}}// --------------说明是红黑树节点--------------else if (f instanceof TreeBin) {validated = true;// 转换为实际类型TreeBin tTreeBin<K,V> t = (TreeBin<K,V>)f;// r 表示红黑树根节点,p表示红黑树中查找到对应key一致的nodeTreeNode<K,V> r, p;if ((r = t.root) != null &&(p = r.findTreeNode(hash, key, null)) != null) {// 保存p.val到pvV pv = p.val;// --------------cv == null为true,那么就是一个删除操作,否则另外就是替换操作--------------if (cv == null || cv == pv ||(pv != null && cv.equals(pv))) {oldVal = pv;if (value != null)p.val = value;// --------------删除操作--------------   else if (t.removeTreeNode(p))setTabAt(tab, i, untreeify(t.first));}}}}}// 当其他线程修改过桶位头结点时,当前线程 sync 头结点锁错对象时,validated为false,会进入下次for自旋if (validated) {if (oldVal != null) {if (value == null)// 如果删除了节点,则要减1addCount(-1L, -1);return oldVal;}break;}}}return null;
}

ConcurrentHashMapremove() 方法步骤

  • 首先遍历整张链表的桶结点,如果表还未初始化或者无法根据参数的 hash 值定位到桶结点,那么将返回 null
  • 如果定位到的桶结点类型是 ForwardingNode 结点,调用 helpTransfer 协助扩容
  • 否则就老老实实的给桶 synchronized 加锁,根据链表或红黑树节点的方式删除一个节点
  • 最后会调用 addCount() 方法 CAS 更新 baseCount 的值

ConcurrentHashMap 的扩容

  • put 方法的详解中,我们可以看到,在链表节点的个数超过 8 的时候,会调用 treeifyBin() 方法来看看是扩容还是转化为红黑树
  • 同时在每次添加完元素的 addCount() 方法中,也会判断当前数组中的元素是否达到了 sizeCtl 的量,如果达到了的话,则会进入 transfer()方法去扩容
 /*** 当数组长度小于64的时候,扩张数组长度一倍,否则的话把链表转为树*/private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; int n, sc;if (tab != null) {if ((n = tab.length) < MIN_TREEIFY_CAPACITY)   tryPresize(n << 1);        // 数组扩容else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {//使用synchronized同步器,将该节点出的链表转为树synchronized (b) {   if (tabAt(tab, index) == b) {TreeNode<K,V> hd = null, tl = null;    //hd:树的头(head)for (Node<K,V> e = b; e != null; e = e.next) {TreeNode<K,V> p =new TreeNode<K,V>(e.hash, e.key, e.val,null, null);// 把Node组成的链表,转化为TreeNode的链表,头结点任然放在相同的位置                      if ((p.prev = tl) == null) hd = p;    //设置headelsetl.next = p;tl = p;}//把TreeNode的链表放入容器TreeBin中setTabAt(tab, index, new TreeBin<K,V>(hd));}}}}
}

可以看到当需要扩容的时候,调用的时候 tryPresize() 方法,看看 trePresize() 的源码

 /*** 扩容表为指可以容纳指定个数的大小(总是2的N次方)* 假设原来的数组长度为16,则在调用tryPresize的时候,size参数的值为16<<1(32),此时sizeCtl的值为12* 计算出来c的值为64,则要扩容到sizeCtl≥为止*  第一次扩容之后 数组长:32 sizeCtl:24*  第二次扩容之后 数组长:64 sizeCtl:48*  第二次扩容之后 数组长:128 sizeCtl:94 --> 这个时候才会退出扩容*/private final void tryPresize(int size) {/** MAXIMUM_CAPACITY = 1 << 30* 如果给定的大小大于等于数组容量的一半,则直接使用最大容量,* 否则使用tableSizeFor算出来* 后面table一直要扩容到这个值小于等于sizeCtrl(数组长度的3/4)才退出扩容*/int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :tableSizeFor(size + (size >>> 1) + 1);int sc;while ((sc = sizeCtl) >= 0) {Node<K,V>[] tab = table; int n;
//            printTable(tab);    调试用的/** 如果数组table还没有被初始化,则初始化一个大小为sizeCtrl和刚刚算出来的c中较大的一个大小的数组* 初始化的时候,设置sizeCtrl为-1,初始化完成之后把sizeCtrl设置为数组长度的3/4* 为什么要在扩张的地方来初始化数组呢?这是因为如果第一次put的时候不是put单个元素,* 而是调用putAll方法直接put一个map的话,在putALl方法中没有调用initTable方法去初始化table,* 而是直接调用了tryPresize方法,所以这里需要做一个是不是需要初始化table的判断*/if (tab == null || (n = tab.length) == 0) {n = (sc > c) ? sc : c;if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {    //初始化tab的时候,把sizeCtl设为-1try {if (table == tab) {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}}}/** 一直扩容到的c小于等于sizeCtl或者数组长度大于最大长度的时候,则退出* 所以在一次扩容之后,不是原来长度的两倍,而是2的n次方倍*/else if (c <= sc || n >= MAXIMUM_CAPACITY) {break;    //退出扩张}else if (tab == table) {int rs = resizeStamp(n);/** 如果正在扩容Table的话,则帮助扩容* 否则的话,开始新的扩容* 在transfer操作,将第一个参数的table中的元素,移动到第二个元素的table中去,* 虽然此时第二个参数设置的是null,但是,在transfer方法中,当第二个参数为null的时候,* 会创建一个两倍大小的table*/if (sc < 0) {Node<K,V>[] nt;if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;/** transfer的线程数加一,该线程将进行transfer的帮忙* 在transfer的时候,sc表示在transfer工作的线程数*/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);}}}
}

tryPresize() 方法中,并没有加锁,允许多个线程进入,如果数组正在扩张,则当前线程也去帮助扩容。数组扩容的主要方法就是 transfer() 方法

/*** 把数组中的节点复制到新的数组的相同位置,或者移动到扩张部分的相同位置* 在这里首先会计算一个步长,表示一个线程处理的数组长度,用来控制对CPU的使用,* 每个CPU最少处理16个长度的数组元素,也就是说,如果一个数组的长度只有16,那只有一个线程会对其进行扩容的复制移动操作* 扩容的时候会一直遍历,知道复制完所有节点,没处理一个节点的时候会在链表的头部设置一个fwd节点,这样其他线程就会跳过他,* 复制后在新数组中的链表不是绝对的反序的*/private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {int n = tab.length, stride;if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)    //MIN_TRANSFER_STRIDE 用来控制不要占用太多CPUstride = MIN_TRANSFER_STRIDE; // subdivide range    //MIN_TRANSFER_STRIDE=16/** 如果复制的目标nextTab为null的话,则初始化一个table两倍长的nextTab* 此时nextTable被设置值了(在初始情况下是为null的)* 因为如果有一个线程开始了表的扩张的时候,其他线程也会进来帮忙扩张,* 而只是第一个开始扩张的线程需要初始化下目标数组*/if (nextTab == null) {            // initiatingtry {@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];nextTab = nt;} catch (Throwable ex) {      // try to cope with OOMEsizeCtl = Integer.MAX_VALUE;return;}nextTable = nextTab;transferIndex = n;}int nextn = nextTab.length;/** 创建一个fwd节点,这个是用来控制并发的,当一个节点为空或已经被转移之后,就设置为fwd节点* 这是一个空的标志节点*/ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);boolean advance = true;    //是否继续向前查找的标志位boolean finishing = false; // to ensure sweep(清扫) before committing nextTab,在完成之前重新在扫描一遍数组,看看有没完成的没for (int i = 0, bound = 0;;) {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);    //设置sizeCtl为扩容后的0.75return;}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}}else if ((f = tabAt(tab, i)) == null)            //数组中把null的元素设置为ForwardingNode节点(hash值为MOVED[-1])advance = casTabAt(tab, i, null, fwd);else if ((fh = f.hash) == MOVED)advance = true; // already processedelse {synchronized (f) {                //加锁操作if (tabAt(tab, i) == f) {Node<K,V> ln, hn;if (fh >= 0) {        //该节点的hash值大于等于0,说明是一个Node节点/** 因为n的值为数组的长度,且是power(2,x)的,所以,在&操作的结果只可能是0或者n* 根据这个规则*         0-->  放在新表的相同位置*         n-->  放在新表的(n+原来位置)*/int runBit = fh & n; Node<K,V> lastRun = f;/** lastRun 表示的是需要复制的最后一个节点* 每当新节点的hash&n -> b 发生变化的时候,就把runBit设置为这个结果b* 这样for循环之后,runBit的值就是最后不变的hash&n的值* 而lastRun的值就是最后一次导致hash&n 发生变化的节点(假设为p节点)* 为什么要这么做呢?因为p节点后面的节点的hash&n 值跟p节点是一样的,* 所以在复制到新的table的时候,它肯定还是跟p节点在同一个位置* 在复制完p节点之后,p节点的next节点还是指向它原来的节点,就不需要进行复制了,自己就被带过去了* 这也就导致了一个问题就是复制后的链表的顺序并不一定是原来的倒序*/for (Node<K,V> p = f.next; p != null; p = p.next) {int b = p.hash & n;    //n的值为扩张前的数组的长度if (b != runBit) {runBit = b;lastRun = p;}}if (runBit == 0) {ln = lastRun;hn = null;}else {hn = lastRun;ln = null;}/** 构造两个链表,顺序大部分和原来是反的* 分别放到原来的位置和新增加的长度的相同位置(i/n+i)*/for (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)/** 假设runBit的值为0,* 则第一次进入这个设置的时候相当于把旧的序列的最后一次发生hash变化的节点(该节点后面可能还有hash计算后同为0的节点)设置到旧的table的第一个hash计算后为0的节点下一个节点* 并且把自己返回,然后在下次进来的时候把它自己设置为后面节点的下一个节点*/ln = new Node<K,V>(ph, pk, pv, ln);else/** 假设runBit的值不为0,* 则第一次进入这个设置的时候相当于把旧的序列的最后一次发生hash变化的节点(该节点后面可能还有hash计算后同不为0的节点)设置到旧的table的第一个hash计算后不为0的节点下一个节点* 并且把自己返回,然后在下次进来的时候把它自己设置为后面节点的下一个节点*/hn = new 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 TreeBin) {    //否则的话是一个树节点TreeBin<K,V> t = (TreeBin<K,V>)f;TreeNode<K,V> lo = null, loTail = null;TreeNode<K,V> hi = null, hiTail = null;int lc = 0, hc = 0;for (Node<K,V> e = t.first; e != null; e = e.next) {int h = e.hash;TreeNode<K,V> p = new TreeNode<K,V>(h, e.key, e.val, null, null);if ((h & n) == 0) {if ((p.prev = loTail) == null)lo = p;elseloTail.next = p;loTail = p;++lc;}else {if ((p.prev = hiTail) == null)hi = p;elsehiTail.next = p;hiTail = p;++hc;}}/** 在复制完树节点之后,判断该节点处构成的树还有几个节点,* 如果≤6个的话,就转回为一个链表*/ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :(hc != 0) ? new TreeBin<K,V>(lo) : t;hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :(lc != 0) ? new TreeBin<K,V>(hi) : t;setTabAt(nextTab, i, ln);setTabAt(nextTab, i + n, hn);setTabAt(tab, i, fwd);advance = true;}}}}}}

引起数组扩容的情况如下:

  • 只有在向 ConcurrentHashMap中添加元素的时候,在链表节点的数目已经超过了 8,同时数组的长度又小于 64 的时候,才会触发数组的扩容
    -当数组中元素达到了 sizeCtl 的数量的时候,则会调用 transfer() 方法来进行扩容

为什么用 synchronized 来代替 ReentrantLock

  • 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized 并不比 ReentrantLock 差,在粗粒度加锁中 ReentrantLock 可能通过 Condition 来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition 的优势就没有了
  • JVM 的开发团队从来都没有放弃 synchronized,而且基于 JVMsynchronized 优化空间更大,使用内嵌的关键字比使用 API 更加自然
  • 在大量的数据操作下,对于 JVM 的内存压力,基于 APIReentrantLock 会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

ConcurrentHashMap 总结

jdk1.8 版本的 ConcurrentHashMap 只是增加了同步的操作来控制并发,从 jdk1.7 版本的 ReentrantLock + Segment(分段锁),到 jdk1.8 版本中 Synchronized + CAS + Volatile

  • jdk.8 的实现降低锁的粒度,jdk1.7 版本锁的粒度是基于 Segment 的,包含多个数组元素,而 jdk1.8 锁的粒度就是每个数组元素
  • jdk1.8 使用的数据结构和 HashMap 相同,即使用数组 + 链表 + 红黑树,其内部数据结构之间的转换过程与 HashMap 相同
  • jdk.8ConcurrentHashMap 在处理 Hash 冲突和 HashMap 是相同的,依然是使用 keyequals() 方法进行比较的
  • jdk.8ConcurrentHashMap 是使用 Synchronized + CAS + Volatile 来保证了线程安全,而 HashMap 未做任何线程安全的处理,相比于 Hashtable 的所有的方法都加上了 synchronized 关键字来说,多线程的情况下,效率提高了很多
  • ConcurrentHashMap 在获取元素时,是没有做任何线程安全的处理的。只有在添加,删除元素时,才使用了 Synchronized + CAS + Volatile 来保证了线程安全

参考1:https://juejin.cn/post/6844903758690779149

Map集合之ConcurrentHashMap相关推荐

  1. Java中的Map集合及其子类HashMap,LinkedHashMap,TreeMap,ConcurrentHashMap

    一 .Map public interface Map<K,V> 将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值.此接口哦取代了Dictionary类,后者完全是 ...

  2. Java集合,ConcurrentHashMap底层实现和原理(常用于并发编程)

    为什么80%的码农都做不了架构师?>>>    概述 ConcurrentHashMap常用于并发编程,这里就从源码上来分析一下ConcurrentHashMap数据结构和底层原理. ...

  3. Map 集合的坑你踩过几个?

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | 公众号「程序通事」 上一篇 List 踩坑文章 ...

  4. c++ map iterator 获取key_JAVA | Map集合使用详解

    引言   了解Set集合如何使用和旗下各类比较,这篇我们继续和大家一起看看Map集合的使用机制. Map Map集合介绍   Map(也称为字典.关联数组)是用于保存具有映射关系的数据,保存两组值,k ...

  5. c++ map 自定义排序_Java学习笔记:Map集合介绍

    在介绍它之前先来看看再API文档中是如何介绍它的,看图片: 由图片可以看出,Map属于双列集合,每次可以添加一对数据,并且这两个数据具有映射关系. 单列集合和双列集合区别 一.Map继承体系 1.Ha ...

  6. Java集合:Map集合

    一.简述 public interface Map<K,V>将键映射到值的对象.一个映射不能包含重复的键:每个键最多只能映射到一个值. 注意:Map中的集合不能包含重复的键,值可以重复.每 ...

  7. Java—Map集合详解(HashMap/Hashtable/LinkedHashMap/Properties/TreeMap/WeakHashMap/IdentityHashMap/EnumMap)

    关注微信公众号:CodingTechWork,一起学习进步. Map Map集合介绍   Map(也称为字典.关联数组)是用于保存具有映射关系的数据,保存两组值,key和value,这两组值可以是任何 ...

  8. java的map集合_Java集合之Map

    正文 Map的特点? 通过Map接口的泛型我们可以看出:Map一次添加一对元素,存储的是键值对:而Collection接口一次添加一个元素. Map接口中的key是唯一的. Map的常见方法? 1.添 ...

  9. 编程坑太多,Map 集合怎么也有这么多坑?一不小心又踩了好几个!| 原力计划...

    作者 | 楼下小黑哥 头图 | CSDN下载自图虫 作为 List 集合好兄弟 Map,我们天天都在使用,一不小心就会踩坑. 今天我就来总结这些常见的坑,再捞自己一手,防止后续同学再继续踩坑. 本文设 ...

  10. 牛客网Java刷题知识点之Java 集合框架的构成、集合框架中的迭代器Iterator、集合框架中的集合接口Collection(List和Set)、集合框架中的Map集合...

    不多说,直接上干货! 集合框架中包含了大量集合接口.这些接口的实现类和操作它们的算法. 集合容器因为内部的数据结构不同,有多种具体容器. 不断的向上抽取,就形成了集合框架. Map是一次添加一对元素. ...

最新文章

  1. Windows10~python和tensorflow配置
  2. CodeForces - 1293C NEKO's Maze Game(思维,水题)
  3. java实现HTTP请求的三种方式
  4. android条形图,MPAndroid组条形图未显示
  5. Python笔记-UiSelector中resourceId定位方式
  6. 解析对象体内与方法体内引用内部方法的不同
  7. Fiddler改包场景04——先拦截请求,修改请求,再拦截响应,修改响应,放行响应
  8. oracle中clob和blob,Oracle中的BLOB和CLOB
  9. openwrt编译基本教程
  10. Docker(从入门到部署微服务集群)
  11. iOS越狱之Mac登录iPhone
  12. Vue中axios的封装
  13. Windows10 最新版官方iso镜像(截至2021/02)win10 2009(20h2)
  14. Linux下Bluetooth编程
  15. 使用Kettle 进行行Oracle数据迁移时处理编码转换处理US7ASCII 编码
  16. 扫地机器人路径规划算法
  17. Resource概述
  18. java 16进制格式化_hutool系列教材 (三)- 编码工具 - Java 16进制工具
  19. python应用: GUI界面设计之JPG转ico工具编辑(PythonMagic)
  20. 保健品nmn是什么东西,nmn稳定性指标说明

热门文章

  1. Kaggle Future Sales“”竞赛 XGB_model_final
  2. 线下商店销量预测挑战赛
  3. 219.存在重复元素II
  4. 简述STL中容器适配器的概念
  5. 内存管理的概念及作用
  6. 分治法实现最大子数组
  7. 用傅里叶分析得到频域信息 MATLAB,信号频谱分析
  8. 凸优化第五章对偶 5.5最优性条件
  9. 【动态主席树】ZOJ 2112【树状数组+主席树】
  10. 有限维线性空间过两点有且只有一条直线的证明