小马源码_Java互联网架构-重新认识Java8-HashMap-不一样的源码解读

欢迎关注头条号:java小马哥

周一至周日早九点半!下午三点半!精品技术文章准时送上!!!

精品学习资料获取通道,参见文末

74bea48239775db0e1b547c7dc7a9585.png

看源码前我们必须先知道一下ConcurrentHashMap的基本结构。ConcurrentHashMap是采用分段锁来进行并发控制的。

其中有一个内部类为Segment类用来表示锁。而Segment类里又有一个HashEntry[]数组,这个数组才是真正用

来存放我们的key-value的。

大概为如下图结构。一个Segment数组,而Segment数组每个元素为一个HashEntry数组

b946fcfba366a3368c729c60a3306173.png

看源码前我们还必须了解的几个默认的常量值:

DEFAULT_INITIAL_CAPACITY = 16 容器默认容量为16

DEFAULT_LOAD_FACTOR = 0.75f 默认扩容因子是0.75

DEFAULT_CONCURRENCY_LEVEL = 16 默认并发度是16

MAXIMUM_CAPACITY = 1 << 30 容器最大容量为1073741824

MIN_SEGMENT_TABLE_CAPACITY = 2 段的最小大小

MAX_SEGMENTS = 1 << 16 段的最大大小

RETRIES_BEFORE_LOCK = 2 通过不获取锁的方式尝试获取size的次数

以上以及默认值是ConcurrentHashMap中定义好的,下面我们很多地方会用到他们。

先从初始化开始说起

通过我们使用ConcurrentHashMap都是通过 ConcurrentHashMap map = new ConcurrentHashMap<>();的方式

我们点进去跟踪下源码

cdf34b8760cfa984b64271f4391b3c44.gif

/**

* Creates a new, empty map with a default initial capacity (16),

* load factor (0.75) and concurrencyLevel (16).

*/

public ConcurrentHashMap() {

this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);

}

cdf34b8760cfa984b64271f4391b3c44.gif

可以看到,默认无参构造函数内调用了另一个带参构造函数,而这个构造函数也就是不管你初始化时传进来什么参数,最终都会跳到那个带参构造函数。

点进去看看这个带参构造函数实现了什么功能

cdf34b8760cfa984b64271f4391b3c44.gif

public ConcurrentHashMap(int initialCapacity,

float loadFactor, int concurrencyLevel) {

if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)

throw new IllegalArgumentException();

if (concurrencyLevel > MAX_SEGMENTS)

concurrencyLevel = MAX_SEGMENTS;

// Find power-of-two sizes best matching arguments

int sshift = 0;

int ssize = 1;

while (ssize < concurrencyLevel) {

++sshift;

ssize <<= 1;

}

this.segmentShift = 32 - sshift;

this.segmentMask = ssize - 1;

if (initialCapacity > MAXIMUM_CAPACITY)

initialCapacity = MAXIMUM_CAPACITY;

int c = initialCapacity / ssize;

if (c * ssize < initialCapacity)

++c;

int cap = MIN_SEGMENT_TABLE_CAPACITY;

while (cap < c)

cap <<= 1;

// create segments and segments[0]

Segment s0 =

new Segment(loadFactor, (int)(cap * loadFactor),

(HashEntry[])new HashEntry[cap]);

Segment[] ss = (Segment[])new Segment[ssize];

UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

this.segments = ss;

}

cdf34b8760cfa984b64271f4391b3c44.gif

我们看到该构造函数一共有三个参数,分别是容器的初始化大小、负载因子、并发度,这三个参数如果我们new 一个ConcurrentHashMap时没有指定,

那么将会采用默认的参数,也就是我们本文开始说的那几个常量值。

在这里我对这三个参数做下解释。容器初始化大小是整个map的容量。负载因子是用来计算每个segment里的HashEntry数组扩容时的阈值的。并发度是

用来设置segment数组的长度的。

6161dfc8da7e6d124efc00fb22821bf1.png

开头这两个if没什么好说的。就是用来判断我们传进来的参数的正确性。当负载因子,初始容量和并发度不按照规范来时会抛出算术异常。第二个if时当传进来的

并发度大于最大段大小的时候,就将其设置为最大段大小。

25a921e2ebf1987c3d4775c39eb1232f.png

这段就比较有意思了。由于segment数组要求长度必须为2的n次方,当我们传进来的并发度不是2的n次方时会计算出一个最接近它的2的n次方值

比如如何我们传进来的并发度为14 15那么通过计算segment数组长度就是16。在上图中我们可以看到两个局部变量ssize和sshift,在循环中如果ssize小于

并发度就将其二进制左移一位,即乘2。因此ssize就是用来保存我们计算出来的最接近并发度的2的n次方值。而ssfhit是用来计算偏移量的。在这里我们又

要说两个很重要的全局常量。segmentMask和segmentShift。其中segmentMask为ssize - 1,由于ssize为2的倍数。那么segmentMask就是奇数。化为

二进制就是全1,而segmentShift为32 - sshift大小。32是key值经过再hash求出来的值的二进制位。segmentMask和segmentShift是用来定位当前元素

在segment数组那个位置,和在HashEntry数组的哪个位置,后面我们会详细说说怎么算的。

b6514f09bf3fe0ee8e2eb5e6ab58455b.png

这一段代码就是用来确定每个segment里面的hashentry的一些参数和初始化segment数组了。第一个if是防止我们设置的初始化

容量大于最大容量。而c是用来计算每个hashentry数组的容量。由于每个hashentry数组容量也需要为2的n次方,因此这里也需要

一个cap和循环来计算一个2的n次方值,方法和上面一样。这里计算出来的cap值就是最终hashentry数组实际的大小了。

初始化就做了这些工作了。

那么我们在说说最简单的get方法。

get方法就需要用到定位我们的元素了。而定位元素就需要我们上面初始化时设置好的两个值:segmentMask和segmentShift

上面说了,并发度默认值为16,那么ssize也为16,因此segmentMask为15.由于ssize二进制往左移了4位,那么sshift就是4,

segmentShift就是32-4=28.下面我们就用segmentMask=15,segmentShift为28来说说怎么确定元素位置的。

在这里我们要说下hash值,这里的hash值不是key的hashcode值,而是经过再hash确定下来的一个hash值,目的是为了减少hash冲突。

hash值二进制为32位。

113031642ead9f82de2b397ddb6e130d.png

上图两个红框就是分别确定segment数组中的位置和hashentry数组中的位置。

我们可以看到确定segment数组是采用 (h >>> segmentShift) & segmentMask,其中h为再hash过的hash值。将32为的hash值往右移segmentShift位。这里我们假设移了28位。

而segmentMask为15,就是4位都为一的二进制。将高4位与segmentMask相与会等到一个小于16的值,就是当前元素再的segment位置。

确定了所属的segment后。就要确认在的hashentry位置了。通过第二个红框处,我们可以看到确定hashentry的位置没有使用上面两个值了。而是直接使用当前hashentry数组的长度减一

和hash值想与。通过两种不同的算法分别定位segment和hashenrty可以保证元素在segment数组和hashentry数组里面都散列开了。

Put方法

cdf34b8760cfa984b64271f4391b3c44.gif

public V put(K key, V value) {

Segment s;

if (value == null)

throw new NullPointerException();

int hash = hash(key);

int j = (hash >>> segmentShift) & segmentMask;

if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck

(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment

s = ensureSegment(j);

return s.put(key, hash, value, false);

}

cdf34b8760cfa984b64271f4391b3c44.gif
cdf34b8760cfa984b64271f4391b3c44.gif

final V put(K key, int hash, V value, boolean onlyIfAbsent) {

HashEntry node = tryLock() ? null :

scanAndLockForPut(key, hash, value);

V oldValue;

try {

HashEntry[] tab = table;

int index = (tab.length - 1) & hash;

HashEntry first = entryAt(tab, index);

for (HashEntry e = first;;) {

if (e != null) {

K k;

if ((k = e.key) == key ||

(e.hash == hash && key.equals(k))) {

oldValue = e.value;

if (!onlyIfAbsent) {

e.value = value;

++modCount;

}

break;

}

e = e.next;

}

else {

if (node != null)

node.setNext(first);

else

node = new HashEntry(hash, key, value, first);

int c = count + 1;

if (c > threshold && tab.length < MAXIMUM_CAPACITY)

rehash(node);

else

setEntryAt(tab, index, node);

++modCount;

count = c;

oldValue = null;

break;

}

}

} finally {

unlock();

}

return oldValue;

}

cdf34b8760cfa984b64271f4391b3c44.gif

上面两片代码就是put一个元素的过程。由于Put方法里需要对共享变量进行写入操作,因此为了安全,需要在操作共享变量时加锁。put时先定位到segment,然后在segment里及逆行擦汗如操作。

插入有两个步骤,第一步判断是否需要对segment里的hashenrty数组进行扩容。第二步是定位添加元素的位置,然后将其放在hashenrty数组里。

我们先说说扩容。

d1013d071e6a9c07c7c45ae2bd9d5815.png

在插入元素的时候会先判断segment里面的hashenrty数组是否超过容量threshold。这个容量是我们刚开始初始化hashenrty数组时采用容量大小和负载因子计算出来的。

如果超过这个阈值(threshold)那么就会进行扩容。扩容括的时当前hashenrty而不是整个map。

如何扩容

扩容的时候会先创建一个容量是原来两个容量大小的数组,然后将原数组里的元素进行再散列后插入到新的数组里。

Size方法

由于map里的元素是遍布所有hashenrty的。因此统计size的时候需要统计每个hashenrty的大小。由于是并发环境下,可能出现有线程在插入或者删除的情况。因此会出现

错误。我们能想到的就是使用size方法时把所有的segment的put,remove和clean方法都锁起来。但是这种方法时很低效的。因此concurrenthashmap采用了以下办法:

先尝试2次通过不加锁的方式来统计各个segment大小,如果统计的过程中,容器的count发生了变化,再采用加锁的方式来统计所有segment的大小。

concurrenthashmap时使用modcount变量来判断再统计的时候容器是否放生了变化。在put、remove、clean方法里操作数据前都会将辩能力modCount进行加一,那么在统计

size千后比较modCount是否发生变化,就可以知道容器大小是否发生变化了。

封面图源网络,侵权删除)

私信头条号,发送:“资料”,获取更多“秘制” 精品学习资料

如有收获,请帮忙转发,您的鼓励是作者最大的动力,谢谢!

一大波微服务、分布式、高并发、高可用的原创系列文章正在路上,

欢迎关注头条号:java小马哥

周一至周日早九点半!下午三点半!精品技术文章准时送上!!!

十余年BAT架构经验倾囊相授

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

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

相关文章

安装默认报表服务器虚拟目录,报表服务器虚拟目录(Reporting Services 配置)

报表服务器虚拟目录(Reporting Services 配置)12/15/2008本文内容使用“报表服务器虚拟目录”页可以配置报表服务器的虚拟目录。用于访问报表服务器 Web 服务的 URL 将包含该虚拟目录名称。完整的 URL 包括前缀(http:// 或 https://)、服务器名称和虚拟目录。服务器名称可能是内…

小程序向webview传参_独家 | 支付宝小程序向个人开发者开放公测

基于兴趣和周围小群体开发的个人小程序&#xff0c;才是为支付宝提供更加多样化的生活服务场景的来源。文 | Tech星球 (微信ID&#xff1a;tech618) 尹非凡、刘宁宁2月26日&#xff0c;Tech星球(微信ID&#xff1a;tech618) 独家获悉&#xff0c;支付宝小程序今日正式面向个人…

原神服务器维护后抽奖池会更新吗,原神:更新维护一小时,补偿60原石,玩家祈求多维护几天!...

10月21号&#xff0c;原神社区发布公告&#xff0c;游戏将会在10月22号7点至11点进行停服维护&#xff0c;所有玩家在这个时间段将无法进入游戏。而作为补偿&#xff0c;官方会赠送5级以上的玩家240原石(停服一小时送60原石)。这是偷偷的更新吗&#xff1f;官方并没有说更新内容…

涉及子模块_COMSOL Multiphysics 5.6 RF模块更新详解

业界领先的多物理场仿真、App 设计与部署的软件解决方案提供商COMSOL 公司发布了全新的COMSOL Multiphysics 软件5.6 版本。新版本为多核和集群计算提供了计算速度更快且内存需求更低的求解器、更加高效的CAD 装配处理功能、仿真App 布局模板&#xff0c;以及一系列包括剪裁平面…

系统参数shell服务器,shell 调用远程服务器shell

shell 调用远程服务器shell 内容精选换一换流程定义文件描述业务逻辑的XML文件&#xff0c;包括workflow.xml、coordinator.xml、bundle.xml三类&#xff0c;最终由Oozie引擎解析并执行。描述业务逻辑的XML文件&#xff0c;包括workflow.xml、coordinator.xml、bundle.xml三类&…

endnote国标_Citavi 与 Endnote 在 Word 插入引用,哪个更适合你?

前言&#xff1a;不黑、不吹&#xff0c;客观讨论&#xff0c;如有补充请留言&#xff0c;我们一定完善内容。我们先看下两者在 Word 界面的显示截图&#xff1a;Endnote &#xff1a;&#xff08;看起来很简洁&#xff09;Citavi &#xff1a;&#xff08;看起来功能多一些&am…

思科服务器如何修改启动项,思科配置tftp服务器

思科配置tftp服务器 内容精选换一换使用mount命令挂载文件系统到云服务器&#xff0c;云服务器系统提示timed out。原因1&#xff1a;网络状态不稳定。原因2&#xff1a;网络连接异常。原因3&#xff1a;云服务器DNS配置错误&#xff0c;导致解析不到文件系统的域名&#xff0c…

社保费客户端显示服务器连接异常,社保费客户端登录服务器异常

社保费客户端登录服务器异常 内容精选换一换本章节指导您使用MongoDB客户端&#xff0c;通过弹性云服务器内网方式连接GaussDB(for Mongo)集群实例。操作系统使用场景&#xff1a;弹性云服务器的操作系统以Linux为例&#xff0c;客户端本地使用的计算机系统以Windows为例。目标…

双继承_在Python中使用双下划线防止类属性被覆盖!

在使用Python编写面向对象的代码时&#xff0c;我们会常常使用“继承”这种开发方式。例如下面这一段代码&#xff1a;class Info:def __init__(self):passdef calc_age(self):print(我是父类的方法) class PeopleInfo(Info):def __init__(self):super().__init__()def calc_ag…

云服务器 自有操作系统,云服务器 自有操作系统

云服务器 自有操作系统 内容精选换一换监控是保持云耀云服务器可靠性、可用性和性能的重要部分&#xff0c;通过监控&#xff0c;用户可以观察云耀云服务器资源。为使用户更好地掌握自己的云耀云服务器运行状态&#xff0c;公有云平台提供了云监控。您可以使用该服务监控您的云…

分割线不显示_90后都30岁了,为什么还不结婚

2020年中国第一批90后已经30岁了。在传统观念里&#xff0c;30岁作为人生的分水岭&#xff0c;成家&#xff0c;立业&#xff0c;结婚&#xff0c;生子&#xff0c;通通要在这之前解决掉&#xff0c;才算赶上了&#xff0c;人生的进度条&#xff0c;然而媒体针对90后&#xff0…

点到线段的距离_直线垂直,垂线的性质,点到直线的距离

欢迎关注公z号&#xff1a;沈阳奥数两条直线相交所成的四个角中&#xff0c;有一个角是直角时&#xff0c;就说这两条直线互相垂直&#xff0c;其中一条直线叫做另一条直线的垂线&#xff0c;它们的交点叫垂足。如图&#xff0c;直线AB与CD垂直于点E&#xff0c;记作&#xff1…

图片 过度曝光_解读:摄影初学者,如何理性处理“曝光不足”与“曝光过度”...

曝光是摄影的基本要素之一&#xff0c;但是许多摄影初学者在曝光不足和过度曝光的问题上经常会遇到很多的困扰&#xff0c;甚至完全不知道如何处理这些问题。其实知道如何获得正确的曝光&#xff0c;并不是你了解曝光过度和曝光不足照片区别的唯一原因。因为创造性的表达比技术…

win7电脑误删鼠标键盘驱动_鼠标键盘,教您怎么解决键盘和鼠标失灵的问题

有的时候在我们使用电脑的过程中会突然间有键盘鼠标失灵的情况发生&#xff0c;而我们都是不明所以、不知所措的。对此&#xff0c;小编我给你们找了解决方法。接下来&#xff0c;就让我们一起往下看看关于键盘鼠标失灵的解决方法吧。键盘和鼠标都是电脑的重要组成部分&#xf…

airpods删除别人的配对_怎么不让别人连我的airpods

airpods很容易就被朋友拿混了&#xff0c;到时候分不清自己的airpods耳机是一件很尴尬的事情。那么&#xff0c;airpods如何避免和别人混拿&#xff1f;不拿出来是最好的解决办法&#xff0c;也可以提前设置不让别人连我的airpods&#xff0c;这样是最靠谱的方法。怎么不让别人…

jmeter安装包双击没反应_windows环境下Jmeter5.2的安装使用

标签&#xff1a;target 首页 环境变量 百度搜索 bsp nbsp htm targe oracl一、安装配置JDKJmeter5.2依赖JDK1.8 版本&#xff0c;JDK安装百度搜索JAVA下载JDK&#xff0c;地址&#xff1a;https://www.oracle.com/technetwork/java/javase/downloads/index.ht…

kafka和mysql内存机制_一文五分钟让你彻底理解Kafka架构原理

对于kafka的架构原理我们先提出几个问题?1.Kafka的topic和分区内部是如何存储的&#xff0c;有什么特点&#xff1f;2.与传统的消息系统相比,Kafka的消费模型有什么优点?3.Kafka如何实现分布式的数据存储与数据读取?一、Kafka架构图1.kafka名词解释在一套kafka架构中有多个P…

手机apk签名工具安卓版_小飞鱼APK签名工具使用方法

小飞鱼APK签名工具是小飞鱼旗下的一款APK签名软件。是移动开发者必备的一款软件。使用方法1、双击运行小飞鱼APK签名工具.exe文件&#xff0c;出现如下界面&#xff1a;2、点击“功能”,没有签名证书的选择证书制作&#xff0c;制作完证书再选择APK签名&#xff0c;有证书的直接…

React 路由传参

引言 在上一篇中&#xff0c;我们学习了 React 中使用路由技术&#xff0c;以及如何使用 MyNavLink 去优化使用路由时的代码冗余的情况。 这一节我们继续上一篇 React 路由进行一些补充 1. Switch 解决相同路径问题 首先我们看一段这样的代码 <Route path"/home&q…

mysql heartbeat lvs_mysql+heartbeat+DRBD+LVS实现mysql高可用二

上一节&#xff0c;讲述了DRBD的安装&#xff0c;因为要利用heartbeatDRBDmysql实现mysql的高可用&#xff0c;所以这一节讲述安装mysql和heartbeat的安装分别在各个节点安装mysql&#xff0c;文本使用的是二进制的安装包mysql-5.5.33-linux2.6-x86_64.tar.gz[rootdrbd1 usr]# …