写点什么

ConcurrentHashMap1-8 源码解读及如何保证线程安全

  • 2022 年 4 月 16 日
  • 本文字数:10075 字

    阅读完需:约 33 分钟

常用方法

put

调用 putVal 方法,并且 putVal 的第三个参数设置为 false


当设置为 false 的时候表示这个 value 一定会设置


true 的时候,只有当这个 key 的 value 为空的时候才会设置


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

putVal 方法

final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException(); // 键或值为空,抛出异常// 键的 hash 值经过计算获得 hash 值 int hash = spread(key.hashCode());int binCount = 0;for (Node<K,V>[] tab = table;;) { // 无限循环 Node<K,V> f; int n, i, fh;if (tab == null || (n = tab.length) == 0) // 表为空或者表的长度为 0// 初始化表 tab = initTable();else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 表不为空并且表的长度大于 0,并且该桶不为空 if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null))) // 比较并且交换值,如 tab 的第 i 项为空则用新生成的 node 替换 break; // no lock when adding to empty bin}else if ((fh = f.hash) == MOVED) // 该结点的 hash 值为 MOVED// 进行结点的转移(在扩容的过程中)tab = helpTransfer(tab, f);else {V oldVal = null;synchronized (f) { // 加锁同步 if (tabAt(tab, i) == f) { // 找到 table 表下标为 i 的节点 if (fh >= 0) { // 该 table 表中该结点的 hash 值大于 0// binCount 赋值为 1binCount = 1;for (Node<K,V> e = f;; ++binCount) { // 无限循环 K ek;if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) { // 结点的 hash 值相等并且 key 也相等// 保存该结点的 val 值 oldVal = e.val;if (!onlyIfAbsent) // 进行判断// 将指定的 value 保存至结点,即进行了结点值的更新 e.val = value;break; 《一线大厂 Java 面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》开源}// 保存当前结点 Node<K,V> pred = e;if ((e = e.next) == null) { // 当前结点的下一个结点为空,即为最后一个结点// 新生一个结点并且赋值给 next 域 pred.next = new Node<K,V>(hash, key,value, null);// 退出循环 break;}}}else if (f instanceof TreeBin) { // 结点为红黑树结点类型 Node<K,V> p;// binCount 赋值为 2binCount = 2;if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) { // 将 hash、key、value 放入红黑树// 保存结点的 valoldVal = p.val;if (!onlyIfAbsent) // 判断// 赋值结点 value 值 p.val = value;}}}}if (binCount != 0) { // binCount 不为 0if (binCount >= TREEIFY_THRESHOLD) // 如果 binCount 大于等于转化为红黑树的阈值// 进行转化 treeifyBin(tab, i);if (oldVal != null) // 旧值不为空// 返回旧值 return oldVal;break;}}}// 增加 binCount 的数量 addCount(1L, binCount);return null;}


1.8 的 put 中涉及 table 的初始化,树的转化,扩容等方法 initTable、tabAt、casTabAt、helpTransfer、putTreeVal、treeifyBin、addCount 函数。后面会一一介绍

1.8putVal 方法流程总结

1.当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,如果没有的话就初始化数组


2.通过计算 hash 值来确定放在数组的哪个位置


3.如果这个位置为空则直接添加,如果不为空的话,则取出这个节点来


4.如果取出来的节点的 hash 值是 MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制


5.最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过 synchronized 来加锁,进行添加操作


6.然后判断当前取出的节点位置存放的是链表还是树


7.如果是链表的话,则遍历整个链表,直到取出来的节点的 key 来个要放的 key 进行比较,如果 key 相等,并且 key 的 hash 值也相等的话,


则说明是同一个 key,则覆盖掉 value,否则的话则添加到链表的末尾


8.如果是树的话,则调用 putTreeVal 方法把这个元素添加到树中去


最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到 8 个以上了的话,


9.则调用 treeifyBin 方法来尝试将处的链表转为树,或者扩容数组

JDK1.8initTable()

在使用的时候才会初始化数组


并发问题是通过 CAS 操控 sizeCtl 变量实现的。

initTable 源码

private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) { //第一次 put 的时候,table 还没被初始化,进入 whileif ((sc = sizeCtl) < 0) //sizeCtl 初始值为 0,当小于 0 的时候表示在别的线程在初始化表或扩展表 Thread.yield(); // lost initialization race; just spinelse if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //SIZECTL:表示当前对象的内存偏移量,sc 表示期望值,-1 表示要替换的值,设定为-1 表示要初始化表了 try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY; //指定了大小的时候就创建指定大小的 Node 数组,否则创建指定大小(16)的 Node 数组 @SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc; //初始化后,sizeCtl 长度为数组长度的 3/4}break;}}return tab;}

initTable 流程

1.如果 sizeCtl 小于 0,说明别的数组正在进行初始化,则让出执行权


2.如果 sizeCtl 大于 0 的话,则初始化一个大小为 sizeCtl 的数组


3.否则的话初始化一个默认大小(16)的数组


4.然后设置 sizeCtl 的值为数组长度的 3/4

tabAt 函数

@SuppressWarnings("unchecked")static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {//强制去读取主存中的数据而不是线程自己的本地工作内存,保证数据是最新的 return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);


}


此函数返回 table 数组中下标为 i 的结点,可以看到是通过 Unsafe 对象通过反射获取的,getObjectVolatile 的第二项参数为下标为 i 的偏移地址。

casTabAt 函数

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {//使用 cas 的比较交换 return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);}


此函数用于比较 table 数组下标为 i 的结点是否为 c,若为 c,则用 v 交换操作。否则,不进行交换操作。

helpTransfer 函数

/**


  • Helps transfer if a resize is in progress.

  • 如果正在调整大小则帮忙转移*///将旧的数组中的元素复制到新的数组中 final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {Node<K,V>[] nextTab; int sc;//旧数组不为空且 nextTable 也不空的情况下才能复制 if (tab != null && (f instanceof ForwardingNode) &&(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {int rs = resizeStamp(tab.length);while (nextTab == nextTable && table == tab &&(sc = sizeCtl) < 0) {if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || transferIndex <= 0)break;//cas 操作保证线程安全 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {transfer(tab, nextTab);//调用扩容方法 break;}}return nextTab;}return table;}


用于在扩容时将 table 表中的结点转移到 nextTable 中。

putTreeVal 函数

final TreeNode<K,V> putTreeVal(int h, K k, V v) {Class<?> kc = null;// 定义 k 的 Class 对象// 标识是否已经遍历过一次树,未必是从根节点遍历的,但是遍历路径上一定已经包含了后续需要比对的所有节点 boolean searched = false;// 从根节点开始遍历,没有终止条件,只能从内部退出 for (TreeNode<K,V> p = root;;) {int dir, ph; K pk;//如果根节点为空,那么赋值当前节点就是根节点,结束循环 if (p == null) {first = root = new TreeNode<K,V>(h, k, v, null, null);break;}// 如果当前节点 hash 大于 指定 key 的 hash 值 else if ((ph = p.hash) > h)dir = -1;//要添加的元素应该放置在当前节点的左侧 else if (ph < h)//小于 dir = 1;//在右侧 else if ((pk = p.key) == k || (pk != null && k.equals(pk)))// 如果当前节点的键对象 和 指定 key 对象相同 return p; // 那么就返回当前节点对象,在外层方法会对 v 进行写入


// 走到这一步说明 当前节点的 hash 值 和 指定 key 的 hash 值 是相等的,但是 equals 不等


else if ((kc == null &&(kc = comparableClassFor(k)) == null) ||(dir = compareComparables(kc, k, pk)) == 0) {


// 走到这里说明:指定 key 没有实现 comparable 接口 或者 实现了 comparable 接口并且和当前节点的键对象比较之后相等(仅限第一次循环)


if (!searched) {// 如果还没有比对过当前节点的所有子节点 TreeNode<K,V> q, ch;// 定义要返回的节点、和子节点 searched = true;//标识为已经遍历过/*


  • 红黑树也是二叉树,所以只要沿着左右两侧遍历寻找就可以了

  • 这是个短路运算,如果先从左侧就已经找到了,右侧就不需要遍历了

  • find 方法内部还会有递归调用*/if (((ch = p.left) != null &&(q = ch.findTreeNode(h, k, kc)) != null) ||((ch = p.right) != null &&(q = ch.findTreeNode(h, k, kc)) != null))return q;}// 走到这里就说明,遍历了所有子节点也没有找到和当前键 equals 相等的节点 dir = tieBreakOrder(k, pk);// 再比较一下当前节点键和指定 key 键的大小}


TreeNode<K,V> xp = p; // 定义 xp 指向当前节点/*


  • 如果 dir 小于等于 0,那么看当前节点的左节点是否为空,如果为空,就可以把要添加的元素作为当前节点的左节点,如果不为空,还需要下一轮继续比较

  • 如果 dir 大于等于 0,那么看当前节点的右节点是否为空,如果为空,就可以把要添加的元素作为当前节点的右节点,如果不为空,还需要下一轮继续比较

  • 如果以上两条当中有一个子节点不为空,这个 if 中还做了一件事,那就是把 p 已经指向了对应的不为空的子节点,开始下一轮的比较*/if ((p = (dir <= 0) ? p.left : p.right) == null) {// 如果恰好要添加的方向上的子节点为空,此时节点 p 已经指向了这个空的子节点 TreeNode<K,V> x, f = first; // 获取当前节点的 next 节点 first = x = new TreeNode<K,V>(h, k, v, f, xp);// 创建一个新的树节点 if (f != null)f.prev = x;if (dir <= 0)xp.left = x; // 左孩子指向到这个新的树节点 elsexp.right = x;// 右孩子指向到这个新的树节点 if (!xp.red)x.red = true;else {lockRoot();//加锁 try {root = balanceInsertion(root, x);// 重新平衡,以及新的根节点置顶} finally {unlockRoot();//解锁}}break;}}assert checkInvariants(root);return null;}


此函数用于将指定的 hash、key、value 值添加到红黑树中,若已经添加了,则返回 null,否则返回该结点。

treeifyBin 函数

当数组长度小于 64 的时候,扩张数组长度一倍,否则的话把链表转为树


private final void treeifyBin(Node<K,V>[] tab, int index) {Node<K,V> b; int n, sc;if (tab != null) {System.out.println("treeifyBin 方\t==>数组长:"+tab.length);if ((n = tab.length) < MIN_TREEIFY_CAPACITY) //MIN_TREEIFY_CAPACITY 64tryPresize(n << 1); // 数组扩容 else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {synchronized (b) { //使用 synchronized 同步器,将该节点出的链表转为树 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);if ((p.prev = tl) == null) //把 Node 组成的链表,转化为 TreeNode 的链表,头结点任然放在相同的位置 hd = p; //设置 headelsetl.next = p;tl = p;}setTabAt(tab, index, new TreeBin<K,V>(hd));//把 TreeNode 的链表放入容器 TreeBin 中}}}}}


此函数用于将桶中的数据结构转化为红黑树,其中,值得注意的是,当 table 的长度未达到阈值时,会进行一次扩容操作,该操作会使得触发 treeifyBin 操作的某个桶中的所有元素进行一


次重新分配,这样可以避免某个桶中的结点数量太大。

tryPresize 方法

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

  • 如果数组 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 方法中,并没有加锁,允许多个线程进入,如果数组正在扩张,则当前线程也去帮助扩容。

addCount 方法

rivate final void addCount(long x, int check) {CounterCell[] as; long b, s;if ((as = counterCells) != null ||!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) { // counterCells 不为空或者比较交换失败 CounterCell a; long v; int m;// 无竞争标识 boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[ThreadLocalRandom.getProbe() & m]) == null ||!(uncontended =U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) { // casfullAddCount(x, uncontended);return;}if (check <= 1)return;s = sumCount();}//是否需要进行扩容 if (check >= 0) {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) {if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||transferIndex <= 0)break;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();}}}


此函数主要完成 binCount 的值加 1 的操作。

扩容方法

jdk1.8transfer 方法

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 被 Java 开源项目【ali1024.coding.net/public/P7/Java/git】 设置值了(在初始情况下是为 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

  • 根据这个规则


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

特点

1.把数组中的节点复制到新的数组的相同位置,或者移动到扩张部分的相同位置


2.在这里首先会计算一个步长,表示一个线程处理的数组长度,用来控制对 CPU 的使用,


3.每个 CPU 最少处理 16 个长度的数组元素,也就是说,如果一个数组的长度只有 16,那只有一个线程会对其进行扩容的复制移动操作


4.扩容的时候会一直遍历,直到复制完所有节点,每处理一个节点的时候会在链表的头部设置一个 fwd 节点,这样其他线程就会跳过他,


5.复制后在新数组中的链表不是绝对的反序的

get 方法

get 方法很简单,支持并发操作,也不加锁


1.当 key 为 null 的时候回抛出 NullPointerException 的异常


2.get 操作通过首先计算 key 的 hash 值来确定该元素放在数组的哪个位置


3.然后遍历该位置的所有节点


4.如果存在返回值,如果不存在的话返回 null


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) { // 表不为空并且表的长度大于 0 并且 key 所在的桶不为空 if ((eh = e.hash) == h) { // 表中的元素的 hash 值与 key 的 hash 值相等 if ((ek = e.key) == key || (ek != null && key.equals(ek))) // 键相等// 返回值 return e.val;}else if (eh < 0) // 结点 hash 值小于 0// 在桶(链表/红黑树)中查找 return (p = e.find(h, key)) != null ? p.val : null;while ((e = e.next) != null) { // 对于结点 hash 值大于 0 的情况 if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null;}

size 方法

总结

面试难免让人焦虑不安。经历过的人都懂的。但是如果你提前预测面试官要问你的问题并想出得体的回答方式,就会容易很多。


此外,都说“面试造火箭,工作拧螺丝”,那对于准备面试的朋友,你只需懂一个字:刷!


给我刷刷刷刷,使劲儿刷刷刷刷刷!今天既是来谈面试的,那就必须得来整点面试真题,这不花了我整 28 天,做了份“Java 一线大厂高岗面试题解析合集:JAVA 基础-中级-高级面试+SSM 框架+分布式+性能调优+微服务+并发编程+网络+设计模式+数据结构与算法等”



且除了单纯的刷题,也得需准备一本【JAVA 进阶核心知识手册】:JVM、JAVA 集合、JAVA 多线程并发、JAVA 基础、Spring 原理、微服务、Netty 与 RPC、网络、日志、Zookeeper、Kafka、RabbitMQ、Hbase、MongoDB、Cassandra、设计模式、负载均衡、数据库、一致性算法、JAVA 算法、数据结构、加密算法、分布式缓存、Hadoop、Spark、Storm、YARN、机器学习、云计算,用来查漏补缺最好不过。



用户头像

还未添加个人签名 2022.04.13 加入

还未添加个人简介

评论

发布
暂无评论
ConcurrentHashMap1-8源码解读及如何保证线程安全_Java_爱好编程进阶_InfoQ写作平台