在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。

本文主要描述的是java多线程中高频面试点concurrentHashMap,concurrentHashMap在实现上和hashMap有很多相似之处。比如底层的数据结构,扩容的倍数,计算table值等等。和hashMap最大的区别就是如何保证线程安全,下面我们也会重点描述其线程安全的保证。如果对hashMap的内容感兴趣,可以看下公众号中之前的文章,有关于hashmap的面试和hashmap的源码解析。如果对java多线程感兴趣的同学可以看下公众号里多线程系列的文章,也许会对你有些帮助。

ConcurrentHashMap的源码有看过,说说其put和get流程?

concurrentHashMap是concurrent包中的重点内容,其put和get过程和hashMap的实现有一定的相似之处,毕竟底层的数据结构是一致的如下。

put过程

final V putVal(K key, V value, boolean onlyIfAbsent) {  if (key == null || value == null) throw new NullPointerException();  int hash = spread(key.hashCode());//计算hash值,这个hash的计算是为了达到更均匀的分布  int binCount = 0;  for (Node<K,V>[] tab = table;;) {  Node<K,V> f; int n, i, fh;  if (tab == null || (n = tab.length) == 0)//如果还没初始化,直接初始化  tab = initTable();  else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果该位置暂时没有值  if (casTabAt(tab, i, null,  new Node<K,V>(hash, key, value, null)))  break;                   // cas添加第一个值  }  else if ((fh = f.hash) == MOVED)//如果正在扩容  tab = helpTransfer(tab, f);//帮助扩容  else {  V oldVal = null;  synchronized (f) {//锁住该hash桶的头节点  if (tabAt(tab, i) == f) {//如果是第一个节点  if (fh >= 0) {//如果是链表  binCount = 1;  for (Node<K,V> e = f;; ++binCount) {//遍历该链表  K ek;  if (e.hash == hash &&  ((ek = e.key) == key ||  (ek != null && key.equals(ek)))) {//hash和equals相等  oldVal = e.val;  if (!onlyIfAbsent)//直接替换  e.val = value;  break;  }  Node<K,V> pred = e;  if ((e = e.next) == null) {//为空,新增  pred.next = new Node<K,V>(hash, key,  value, null);  break;  }  }  }  else if (f instanceof TreeBin) {//红黑树节点  Node<K,V> p;  binCount = 2;  if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,  value)) != null) {//添加节点  oldVal = p.val;  if (!onlyIfAbsent)  p.val = value;  }  }  }  }  if (binCount != 0) {  if (binCount >= TREEIFY_THRESHOLD)//判断是否超过门限值  treeifyBin(tab, i);//转换为红黑树  if (oldVal != null)  return oldVal;  break;  }  }  }  addCount(1L, binCount);//添加数据,如果长度超过就扩容  return null;  }  

Put的过程大致可以分为以下几个步骤:

  1. 求出hash值
  2. 是否已经初始化数组,如果没有初始化
  3. 是否该位置为空,空的话直接cas设置第一个节点
  4. 判断是否正在扩容,如果正在扩容,就加入一起进行扩容
  5. 如果不为空的话,锁住头结点,开始进行插入操作,如果是链表,就遍历是否相同,如果是红黑树就直接添加
  6. 添加完成之后,判断是否需要扩容,如果超过阈值就扩容。

上面标红的几个操作是concurrentHashMap和hashMap 的区别,这几个点,就是concurrentHashMap在添加过程保证线程安全的。

Get过程

public V get(Object key) {  Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;  int h = spread(key.hashCode());//求出hashcode值  if ((tab = table) != null && (n = tab.length) > 0 &&  (e = tabAt(tab, (n - 1) & h)) != null) {  if ((eh = e.hash) == h) {  if ((ek = e.key) == key || (ek != null && key.equals(ek)))//如果第一个节点找到了直接返回  return e.val;  }  else if (eh < 0)//如果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;  }  

Get的流程:

  1. 获取hash值
  2. 如果是第一个节点,直接返回
  3. 如果不是,判断是否正在扩容或者是红黑树,那就调用find方法
  4. 如果不是,那就是链表结构,直接while寻找

get过程为什么没有和put过程一样需要加锁?原因在于node中的val是volatile,我们每次取出来的是最新的值,这里使用的是volatile的可见性。

聊聊ConcurrentHashMap扩容的过程?什么是协助扩容?

ConcurrentHashMap的扩容过程允许多个线程同时操作,当一个线程在put的时候,发现正在扩容的话,就会帮助一起扩容,这个过程称为协助扩容。如何判断是否正在扩容,concurrentHashMap使用sizectl来进行控制。我们先看下sizectl的状态变化。

协助扩容过程:

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {  Node<K,V>[] nextTab; int sc;  if (tab != null && (f instanceof ForwardingNode) &&//如果该节点是ForwardingNode,说明有线程正在扩容  (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {//已经扩容的nextTable不为空  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;  if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {//设置sizectl的值  transfer(tab, nextTab);//数据迁移  break;  }  }  return nextTab;//返回新的hash数组  }  return table;//返回旧的hash数组  }  

真正的扩容过程在transfer函数,其实现的核心思路是:设置一个步长,每次搬迁数据的一个步长的数据,搬迁完之后再来申请搬迁下一个步长。源码扩容部分很长,但是建议还是看下。

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)stride = MIN_TRANSFER_STRIDE; // subdivide range// 第一个线程先创建nextTabif (nextTab == null) {try {// 容量翻倍Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];nextTab = nt;} catch (Throwable ex) {      sizeCtl = Integer.MAX_VALUE;return;}nextTable = nextTab; transferIndex = n;}// 新table的长度int nextn = nextTab.length;// ForwardingNode表示已经迁移过的结点,hash值为MOED(-1)ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);boolean advance = true;// advance 标识迁移是否完成boolean finishing = false; // 表识确保可以提交// i 是索引值,bound 是本次迁移的边界值for (int i = 0, bound = 0;;) {Node<K,V> f; int fh;// i = transferIndex,bound = transferIndex-stridewhile (advance) {int nextIndex, nextBound;if (--i >= bound || finishing)advance = false;// 原TABLE的所有位置都有线程去处理了else if ((nextIndex = transferIndex) <= 0) {i = -1;advance = false;//设置为不需要}else if (U.compareAndSwapInt//cas设置边界值(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;// 将新的 nextTab 赋值给 table 属性sizeCtl = (n << 1) - (n >>> 1);// 重新计算 sizeCtlreturn;//返回}// 修改sizeCtl的值if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {// 退出方法if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)return;finishing = advance = true;i = n; // 提交前再次确认}}// 如果位置 i 处是空的,使用cas设置初始化的 fwd节点else if ((f = tabAt(tab, i)) == null)advance = casTabAt(tab, i, null, fwd);// 该位置已经迁移过了else if ((fh = f.hash) == MOVED)advance = true; // 已经处理过了//需要迁移else {synchronized (f) {// 迁移前加锁,开始迁移if (tabAt(tab, i) == f) {Node<K,V> ln, hn;// 链表迁移,将链表一分为二,分别搬移if (fh >= 0) {int runBit = fh & n;Node<K,V> lastRun = f;for (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 (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 Node<K,V>(ph, pk, pv, ln);elsehn = new Node<K,V>(ph, pk, pv, hn);}// 其中的一个链表放在新数组的位置 isetTabAt(nextTab, i, ln);// 另一个链表放在新数组的位置 i+nsetTabAt(nextTab, i + n, hn);// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,setTabAt(tab, i, fwd);// 设置该位置已经迁移完毕advance = true;}else if (f instanceof TreeBin) {// 红黑树的迁移,拆分为2TreeBin<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;}}//节点数少于 8,红黑树转换回链表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;// 将 ln 放置在新数组的位置 isetTabAt(nextTab, i, ln);// 将 hn 放置在新数组的位置 i+nsetTabAt(nextTab, i + n, hn);// 将原数组该位置处设置为 fwd,代表该位置已经处理完毕,setTabAt(tab, i, fwd);// advance 设置为 true,代表该位置已经迁移完毕advance = true;}}}}}}

ConcurrentHashMap是怎么保证线程安全的?

  1. CAS操作数据:sizectl的修改,扩容数值等修改使用cas保证数据修改的原子性。
  2. synchronized互斥锁:put和扩容过程,使用synchronized保证线程只有一个操作,保证线程安全。
  3. volatile修饰变量:table、sizeCtl等变量用volatile修饰,保证可见性

ConcurrentHashMap是面试中高频知识点,本文主要描述和HashMap的区别点,这也是面试中常问的点,结合之前的HashMap一起看下,可能会帮助你更好的理解。本文篇幅相比之前偏长,主要是介绍了concurrentHashMap的源码内容,这个如果有时间可以看看,没时间的话,可以记下过程结论。

本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。

想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

Java多线程篇--concurrentHashMap相关推荐

  1. 面试题汇总二 Java 多线程篇

    前言 题目汇总来源 史上最全各类面试题汇总,没有之一,不接受反驳 面试题汇总一 Java 语言基础篇 面试题汇总二 Java 多线程篇 面试题汇总三 Java 集合篇 面试题汇总四 JVM 篇 面试题 ...

  2. 明翰Java教学系列之多线程篇V0.2(持续更新)

    文章目录 传送门 前言 背景知识 并行与并发 线程与进程 内存模型 1. 计算机内存模型 `2. Java内存模型` 2.1 内存交互 2.1.1 交互操作 2.1.2 交互规则 `2.2 并发编程特 ...

  3. Java总结篇系列:Java多线程(三)

    2019独角兽企业重金招聘Python工程师标准>>> 本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题. 一.一个典型的Java线程安全例子 public cla ...

  4. Java总结篇系列:Java多线程(二)

    本文承接上一篇文章<Java总结篇系列:Java多线程(一)>. 四.Java多线程的阻塞状态与线程控制 上文已经提到Java阻塞的几种具体类型.下面分别看下引起Java线程阻塞的主要方法 ...

  5. Java多线程编程实战指南+设计模式篇pdf

    下载地址:网盘下载 随着CPU 多核时代的到来,多线程编程在充分利用计算资源.提高软件服务质量方面扮演了越来越重要的角色.而 解决多线程编程中频繁出现的普遍问题可以借鉴设计模式所提供的现成解决方案.然 ...

  6. java多线程编程_Java多线程编程实战指南+设计模式篇.pdf

    Java多线程编程实战指南+设计模式篇.pdf 对Java架构技术感兴趣的工程师朋友们可以关注我,转发此文后私信我"Java"获取更多Java编程PDF资料(附送视频精讲) 关注我 ...

  7. Java多线程系列(八):ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)

    HashMap.CurrentHashMap 的实现原理基本都是BAT面试必考内容,阿里P8架构师谈:深入探讨HashMap的底层结构.原理.扩容机制深入谈过hashmap的实现原理以及在JDK 1. ...

  8. java线程安全例子_Java总结篇系列:Java多线程(三)

    本文主要接着前面多线程的两篇文章总结Java多线程中的线程安全问题. 一.一个典型的Java线程安全例子 1 public classThreadTest {2 3 public static voi ...

  9. JAVA多线程基础篇-关键字synchronized

    1.概述 syncronized是JAVA多线程开发中一个重要的知识点,涉及到多线程开发,多多少少都使用过.那么syncronized底层是如何实现的?为什么加了它就能实现资源串行访问?本文将基于上述 ...

  10. Java太密来福_这篇文章就是要让你入门java多线程【多线程入门】-Go语言中文社区...

    就在前几天,有位读者朋友私信宜春,说期待出一篇多线程的文章,我当时内心是小鹿乱撞啊-于是这几天茶不思饭不想,好几天深夜皆是辗转反侧,两目深凝,以至于这几天走起路来格外飘飘然,左摇右晃的,魔鬼般的步伐, ...

最新文章

  1. 【CSS】【9】CSS盒子的浮动
  2. Docker初次见面
  3. android 请求参数打印,android retrofit 请求参数格式RequestBody的方法
  4. 愚蠢的领导才会用程序员祭天!!
  5. (转)SpringMVC学习(八)——SpringMVC中的异常处理器
  6. PHP合并2个数字键数组的值
  7. implements Serializable有什么作用
  8. SSH 连接、远程上传下载文件
  9. 保护心灵窗口——防蓝光软件f.lux
  10. 人力资源管理专业知识与实务(初级)【6】
  11. Overture五线谱打曲谱用得上的排版技巧
  12. hazelcast java_Hazelcast入门教程
  13. tcpdump manual 中文翻译
  14. Zookeeper原理详解
  15. 【设计模式实战】简单工厂、工厂方法、抽象工厂:原理篇
  16. 通达OA漏洞分析合集
  17. debug 进阶 跳过反射以及aop
  18. 台湾本地支付GASH钱包及点卡详细介绍
  19. 80c51汇编语言程序设计,章4 80C51的汇编语言程序设计
  20. Android 12适配安全组件导出设置`android:exported` 指定显式值”

热门文章

  1. 3600S软件测试工资,软件测试工资能拿到多少?谁说软件测试收入低?
  2. java 动态修改配置文件_Java 项目中一种简单的动态修改配置即时生效的方式 WatchService...
  3. 2021年焊工(初级)模拟考试及焊工(初级)作业考试题库
  4. GIS(地理信息系统)基本概念
  5. 头条号项目玩法:中视频全方位教学
  6. 电影影视网站搭建教程
  7. 使用android新特性:Material Design
  8. 二、Esp32开发环境快速搭建(vscode+PlatformIO IED)
  9. 线性代数笔记29——正定矩阵和最小值
  10. Krpano元素的一些解析