面试官:为什么ConcurrentHashMap要放弃分段锁?

今天我们来讨论一下一个比较经典的面试题就是 ConcurrentHashMap 为什么放弃使用了分段锁,这个面试题阿粉相信很多人肯定觉得有点头疼,因为很少有人在开发中去研究这块的内容,今天阿粉就来给大家讲一下这个 ConcurrentHashMap 为什么在 JDK8 中放弃了使用分段锁。

什么是分段锁

我们都知道 HashMap 是一个线程不安全的类,多线程环境下,使用 HashMap 进行put操作会引起死循环,导致CPU利用率接近100%,所以如果你的并发量很高的话,所以是不推荐使用 HashMap 的。

而我们所知的,HashTable 是线程安全的,但是因为 HashTable 内部使用的 synchronized 来保证线程的安全,所以,在多线程情况下,HashTable 虽然线程安全,但是他的效率也同样的比较低下。

所以就出现了一个效率相对来说比 HashTable 高,但是还比 HashMap 安全的类,那就是 ConcurrentHashMap,而 ConcurrentHashMap 在 JDK8 中却放弃了使用分段锁,为什么呢?那他之后是使用什么来保证线程安全呢?我们今天来看看。

什么是分段锁?

其实这个分段锁很容易理解,既然其他的锁都是锁全部,那分段锁是不是和其他的不太一样,是的,他就相当于把一个方法切割成了很多块,在单独的一块上锁的时候,其他的部分是不会上锁的,也就是说,这一段被锁住,并不影响其他模块的运行,分段锁如果这样理解是不是就好理解了,我们先来看看 JDK7 中的 ConcurrentHashMap 的分段锁的实现。

在 JDK7 中 ConcurrentHashMap 底层数据结构是数组加链表,这也是之前阿粉说过的 JDK7和 JDK8 中 HashMap 不同的地方,源码送上。

//初始总容量,默认16
static final int DEFAULT_INITIAL_CAPACITY = 16;
//加载因子,默认0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//并发级别,默认16
static final int DEFAULT_CONCURRENCY_LEVEL = 16;static final class Segment<K,V> extends ReentrantLock implements Serializable {transient volatile HashEntry<K,V>[] table;}

在阿粉贴上的上面的源码中,有Segment<K,V>,这个类才是真正的的主要内容, ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成.

我们看到了 Segment<K,V>,而他的内部,又有HashEntry数组结构组成. Segment 继承自 RentrantLock 在这里充当的是一个锁,而在其内部的 HashEntry 则是用来存储键值对数据.

图就像下面这个样子

4c9e2936dddeb48178cef19aba2c67b4.jpeg

也就是说,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在哪一个分段中,然后对这个分段进行加锁。

最后也就出现了,如果不是在同一个分段中的 put 数据,那么 ConcurrentHashMap 就能够保证并行的 put ,也就是说,在并发过程中,他就是一个线程安全的 Map 。

为什么 JDK8 舍弃掉了分段锁呢?

这时候就有很多人关心了,说既然这么好用,为啥在 JDK8 中要放弃使用分段锁呢?

这就要我们来分析一下为什么要用 ConcurrentHashMap ,

1.线程安全。

2.相对高效。

因为在 JDK7 中 Segment 继承了重入锁ReentrantLock,但是大家有没有想过,如果说每个 Segment 在增长的时候,那你有没有考虑过这时候锁的粒度也会在不断的增长。

而且前面阿粉也说了,一个Segment里包含一个HashEntry数组,每个锁控制的是一段,那么如果分成很多个段的时候,这时候加锁的分段还是不连续的,是不是就会造成内存空间的浪费。

所以问题一出现了,分段锁在某些特定的情况下是会对内存造成影响的,什么情况呢?我们倒着推回去就知道:

1.每个锁控制的是一段,当分段很多,并且加锁的分段不连续的时候,内存空间的浪费比较严重。

大家都知道,并发是什么样子的,就相当于百米赛跑,你是第一,我是第二这种形式,同样的,线程也是这样的,在并发操作中,因为分段锁的存在,线程操作的时候,争抢同一个分段锁的几率会小很多,既然小了,那么应该是优点了,但是大家有没有想过如果这一分块的分段很大的时候,那么操作的时间是不是就会变的更长了。

所以第二个问题出现了:

2.如果某个分段特别的大,那么就会影响效率,耽误时间。

所以,这也是为什么在 JDK8 不在继续使用分段锁的原因。

既然我们说到这里了,我们就来聊一下这个时间和空间的概念,毕竟很多面试官总是喜欢问时间复杂度,这些看起来有点深奥的东西,但是如果你自己想想,用自己的话说出来,是不是就没有那么难理解了。

什么是时间复杂度

百度百科是这么说的:

在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间,
这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数

其实面试官问这个 时间复杂度 无可厚非,因为如果你作为一个公司的领导,如果手底下的两个员工,交付同样的功能提测,A交付的代码,运行时间50s,内存占用12M,B交付的代码,运行时间110s,内存占用50M 的时候,你会选择哪个员工提交的代码?

A 还是 B 这个答案一目了然,当然,我们得先把 Bug 这种因素排除掉,没有任何质疑,肯定选 A 员工提交的代码,因为运行时间快,内存占用量小,那肯定的优先考虑。

那么既然我们知道这个代码都和时间复杂度有关系了,那么面试官再问这样的问题,你还觉得有问题么?

答案也很肯定,没问题,你计算不太熟,但是也需要了解。

我们要想知道这个时间复杂度,那么就把我们的程序拉出来运行一下,看看是什么样子的,我们先从循环入手,

for(i=1; i<=n; i++)
{j = i;j++;
}

它的时间复杂度是什么呢?上面百度百科说用大O符号表述,那么实际上它的时间复杂度就是 O(n),这个公式是什么意思呢?

线性阶 O(n),也就是说,我们上面写的这个最简单的算法的时间趋势是和 n 挂钩的,如果 n 变得越来越大,那么相对来说,你的时间花费的时间也就越来越久,也就是说,我们代码中的 n 是多大,我们的代码就要循环多少遍。这样说是不是就很简单了?

关于时间复杂度,阿粉以后会给大家说,话题跑远了,我们回来,继续说,JDK8 的 ConcurrentHashMap 既然不使用分段锁了,那么他使用的是什么呢?

JDK8 的 ConcurrentHashMap 使用的是什么?

从上面的分析中,我们得出了 JDK7 中的 ConcurrentHashMap 使用的是 Segment 和 HashEntry,而在 JDK8 中 ConcurrentHashMap 就变了,阿粉现在这里给大家把这个抛出来,我们再分析, JDK8 中的 ConcurrentHashMap 使用的是 synchronized 和 CAS 和 HashEntry 和红黑树。

听到这里的时候,我们是不是就感觉有点类似,HashMap 是不是也是使用的红黑树来着?有这个感觉就对了,

20d17c0163a559566464646fc7a9c476.png

ConcurrentHashMap 和 HashMap 一样,使用了红黑树,而在 ConcurrentHashMap 中则是取消了Segment分段锁的数据结构,取而代之的是Node数组+链表+红黑树的结构。

为什么要这么做呢?

因为这样就实现了对每一行数据进行加锁,减少并发冲突。

实际上我们也可以这么理解,就是在 JDK7 中,使用的是分段锁,在 JDK8 中使用的是 “读写锁” 毕竟采用了 CAS 和 Synchronized 来保证线程的安全。

我们来看看源码:

//第一次put 初始化 Node 数组
private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {if ((sc = sizeCtl) < 0)Thread.yield(); // lost initialization race; just spinelse if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}break;}}return tab;}
public V put(K key, V value) {return putVal(key, value, false);}/** Implementation for put and putIfAbsent */final V putVal(K key, V value, boolean onlyIfAbsent) {if (key == null || value == null) throw new NullPointerException();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)tab = initTable();//如果相应位置的Node还未初始化,则通过CAS插入相应的数据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}else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);...//如果该节点是TreeBin类型的节点,说明是红黑树结构,则通过putTreeVal方法往红黑树中插入节点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;}//如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,返回旧值if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);if (oldVal != null)return oldVal;break;}}addCount(1L, binCount);return null;}

put 的方法有点太长了,阿粉就截取了部分代码,大家莫怪,如果大家有兴趣,大家可以去对比一下去 JDK7 和 JDK8 中寻找不同的东西,这样亲自动手才能收获到更多不是么?

文章参考

《百度百科-时间复杂度》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544024.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

面试突击72:输入URL之后会执行什么流程?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;在浏览器中输入 URL 之后&#xff0c;它会执行以下几个流程&#xff1a;执行 DNS 域名解析&#xff1b;封装 HTTP 请…

面试必备:TCP 经典 15 连问!

TCP协议是大厂面试必问的知识点。整理了15道非常经典的TCP面试题&#xff0c;希望大家都找到理想的offer呀1. 讲下TCP三次握手流程开始客户端和服务器都处于CLOSED状态&#xff0c;然后服务端开始监听某个端口&#xff0c;进入LISTEN状态第一次握手(SYN1, seqx)&#xff0c;发…

ISP QoS Lab

ISP QoS Lab1-PQ优先级队列&#xff08;PQ&#xff0c;Priority Queue&#xff09;中&#xff0c;有高、中、普通、低优先级四个队列。数据包根据事先的定义放在不同的队列中&#xff0c;路由器按照高、中、普通、低顺序服务&#xff0c;只有高优先级的队列为空后才为中优先级的…

面渣逆袭:JVM经典五十问,这下面试稳了!

引言1.什么是JVM?JVM——Java虚拟机&#xff0c;它是Java实现平台无关性的基石。Java程序运行的时候&#xff0c;编译器将Java文件编译成平台无关的Java字节码文件&#xff08;.class&#xff09;,接下来对应平台JVM对字节码文件进行解释&#xff0c;翻译成对应平台匹配的机器…

操作系统大内核和微内核_操作系统中的内核类型

操作系统大内核和微内核As we have already studied about the Kernels, we know that the Kernel is a program which is the main component of the Operating System. Now let us study about the types of Kernels. 正如我们已经研究了内核一样 &#xff0c;我们知道内核是…

【论文解读】Learning based fast H.264 to H.265 transcoding

时间&#xff1a; 2015 年 级别&#xff1a;APSIPA 机构&#xff1a; 上海电力大学 摘要 新提出的视频编码标准HEVC (High Efficiency video coding)以其比H.264/AVC更好的编码效率&#xff0c;被工业界和学术界广泛接受和采用。在HEVC实现了约40%的编码效率提升的同时&#…

面试必备:聊聊sql优化的15个小技巧

sql优化是一个大家都比较关注的热门话题&#xff0c;无论你在面试&#xff0c;还是工作中&#xff0c;都很有可能会遇到。如果某天你负责的某个线上接口&#xff0c;出现了性能问题&#xff0c;需要做优化。那么你首先想到的很有可能是优化sql语句&#xff0c;因为它的改造成本…

面试突击73:IoC 和 DI 有什么区别?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;IoC 和 DI 都是 Spring 框架中的重要概念&#xff0c;就像玫瑰花与爱情一样&#xff0c;IoC 和 DI 通常情况下也是成…

MyBatis整合Spring的实现(13)

2019独角兽企业重金招聘Python工程师标准>>> 本章中分析insert元素的解析。 1 配置文件 <insert id"insert" parameterType"cn.vansky.schedule.time.menu.bo.Menu"><!--WARNING - mbggeneratedThis element is automatically generat…

麻了,代码改成多线程,竟有9大问题

很多时候&#xff0c;我们为了提升接口的性能&#xff0c;会把之前单线程同步执行的代码&#xff0c;改成多线程异步执行。比如&#xff1a;查询用户信息接口&#xff0c;需要返回用户基本信息、积分信息、成长值信息&#xff0c;而用户、积分和成长值&#xff0c;需要调用不同…

浅聊一下线程池的10个坑

日常开发中&#xff0c;为了更好管理线程资源&#xff0c;减少创建线程和销毁线程的资源损耗&#xff0c;我们会使用线程池来执行一些异步任务。但是线程池使用不当&#xff0c;就可能会引发生产事故。今天跟大家聊聊线程池的10个坑。大家看完肯定会有帮助的~线程池默认使用无界…

Linux命令行上执行操作,不退回命令行的解决方法

问题描述&#xff1a; 如果你现在登录Centos执行了某个操作&#xff0c;但是操作一直占用命令行&#xff0c;命令行显示的也都是这个命令相关的操作&#xff0c;我想做其它事情 &#xff0c;该怎么办呢 &#xff1f; 解决方法&#xff1a; 根据《Linux命令行与Shell编程大全第2…

SpringBoot 读取配置文件的 5 种方法!

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Spring Boot 中读取配置文件有以下 5 种方法&#xff1a;使用 Value 读取配置文件。使用 ConfigurationProperties 读…

使用阿里巴巴 Druid 轻松实现加密!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;为什么要加密&#xff1f;现在的开发习惯&#xff0c;无论是公司的项目还是个人的项目&#xff0c;都会选择将源码上传到 Gi…

xml不显示css样式_如何使用CSS显示XML?

xml不显示css样式Introduction: 介绍&#xff1a; You must be aware of the term XML and must have dealt with these various XML files while developing a web page or website. This article focuses entirely on XML and how to display them using CSS. There are num…

c#组元(Tuple)的使用

组元(Tuple)是C# 4.0引入的一个新特性&#xff0c;可以在.NET Framework 4.0或更高版本中使用。组元使用泛型来简化类的定义&#xff0c;多用于方法的返回值。在函数需要返回多个类型的时候&#xff0c;就不必使用out , ref等关键字了&#xff0c;直接定义一个Tuple类型&#x…

浅谈一下 MyBatis 批量插入的 3 种方法!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone批量插入功能是我们日常工作中比较常见的业务功能之一&#xff0c;今天咱们来一个 MyBatis 批量插入的汇总篇&#xff0c;同时对 3 种实现…

快速搭建 SpringCloud Alibaba Nacos 配置中心!

作者 | 磊哥来源 | Java中文社群&#xff08;ID&#xff1a;javacn666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;Spring Cloud Alibaba 是阿里巴巴提供的一站式微服务开发解决方案&#xff0c;目前已被 Spring Cloud 官方收录。而 Nacos 作…

浅聊一下建表的15个小技巧

前言对于后端开发同学来说&#xff0c;访问数据库&#xff0c;是代码中必不可少的一个环节。系统中收集到用户的核心数据&#xff0c;为了安全性&#xff0c;我们一般会存储到数据库&#xff0c;比如&#xff1a;mysql&#xff0c;oracle等。后端开发的日常工作&#xff0c;需要…

JConsole的使用手册 JDK1.5(转)

一篇Sun项目主页上介绍JConsole使用的文章&#xff0c;前段时间性能测试的时候大概翻译了一下以便学习&#xff0c;今天整理一下发上来&#xff0c;有些地方也不知道怎么翻&#xff0c;就保留了原文&#xff0c;可能还好理解点&#xff0c;呵呵&#xff0c;水平有限&#xff0c…