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

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

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

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

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

来存放我们的key-value的。

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

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

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<>();的方式

我们点进去跟踪下源码

/**

* 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);

}

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

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

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;

}

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

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

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

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

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

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

这段就比较有意思了。由于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数组的哪个位置,后面我们会详细说说怎么算的。

这一段代码就是用来确定每个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位。

上图两个红框就是分别确定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方法

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

}

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;

}

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

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

我们先说说扩容。

在插入元素的时候会先判断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架构经验倾囊相授

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

  1. Java互联网架构 百度云_java互联网架构师

    资源内容: java互联网架构师|____014_互联网架构视频第二期(017).rar|____013_互联网架构视频第二期(016).rar|____012_互联网架构视频第二期(015).rar ...

  2. jsp 上传转码_Java实现视频网站的视频上传、视频转码、视频关键帧抽图, 及视频播放功能...

    视频网站中提供的在线视频播放功能,播放的都是FLV格式的文件,它是Flash动画文件,可通过Flash制作的播放器来播放该文件.项目中用制作的player.swf播放器. 多媒体视频处理工具FFmpe ...

  3. jsp 上传转码_Java实现视频网站的视频上传、视频转码、及视频播放功能(ffmpeg)...

    视频网站中提供的在线视频播放功能,播放的都是FLV格式的文件,它是Flash动画文件,可通过Flash制作的播放器来播放该文件.项目中用制作的player.swf播放器. 多媒体视频处理工具FFmpe ...

  4. java获取当前周一_Java互联网架构-Spring IOC源码分析

    欢迎关注头条号:java小马哥 周一至周日下午三点半!精品技术文章准时送上!!! 精品学习资料获取通道,参见文末 源码介绍之前,看几个问题: Bean的承载对象是什么? Bean的定义如何存储的? B ...

  5. hashmap 存的是对象的引用地址_Java互联网架构-面试虐我千百遍HashMap源码真讨厌...

    在java的容器集合中,hashmap的使用频率可以说是相当高的.不过对于hashmap的存(put())以及取(get())的原理可能很多人还不大清楚,今天,我就给大家介绍下它是如何存如何取的. # ...

  6. java bean参数清空_Java互联网架构-Spring IOC底层源码分析

    欢迎关注头条号:java小马哥 周一至周日早九点半!下午三点半!精品技术文章准时送上!!! 精品学习资料获取通道,参见文末 spring ioc是spring的核心之一,也是spring体系的基础,那 ...

  7. springboot 自动装配_Java互联网架构-SpringBoot自动装配核心源码剖析

    欢迎关注头条号:java小马哥 周一至周日早九点半!下午三点半!精品技术文章准时送上!!! 精品学习资料获取通道,参见文末 用了差不多两年的SpringBoot了,可以说对SpringBoot已经很熟 ...

  8. 网络多人游戏架构与编程 电子书_Java互联网架构-高性能网络编程必备技能IO与NIO阻塞分析...

    欢迎关注头条号:java小马哥 周一至周日早九点半!下午三点半!精品技术文章准时送上!!! 精品学习资料获取通道,参见文末 一.概念 NIO即New IO,这个库是在JDK1.4中才引入的.NIO和I ...

  9. java session使用_Java互联网架构-高负载集群架构如何解决session一致性问题

    欢迎关注头条号:java小马哥 周一至周日早九点半!下午三点半!精品技术文章准时送上!!! 精品学习资料获取通道,参见文末 本文讲述了一路走来对Session的认知.文章有点长,不过是故事型的,应该不 ...

最新文章

  1. 使用BCH提供的Cryptonize创建自己的加密代币
  2. Linux内存管理原理
  3. miniblink载入html,winform使用miniblink展示html(全屏)
  4. 容器编排技术 -- Kubernetes 组件
  5. Centos7上openVP的另一种使用方式,实现访问控制!
  6. 清北学堂模拟赛d6t2 刀塔
  7. Python selenium 延时的几种方法
  8. android BaseFragment获取Context上下文方法
  9. SQL超级简单的基础入门
  10. ORAN专题系列-5:5G O-RAN 一体式小基站硬件白盒化的参考架构
  11. K3Cloud WebAPI 学习笔记:财务会计-总账-凭证
  12. ionic html5 上传图片,ionic文件选择与ionic文件上传
  13. 【JavaWeb】在office word中使用merge field出现空行问题
  14. python 知乎 合并 pdf_怎么把多个pdf合并在一起?
  15. 【微信开发|PHP】设置关注自动回复,关键词自动回复。
  16. 省编码市编码区县编码_如何摆脱编码的束缚,走向事业
  17. 如何在网页上打印文字?
  18. 【深度学习框架输入格式】NCHW还是NHWC?
  19. 最小二乘拟合n阶多项式【Matlab】
  20. eclipse自动排版快捷键 按了没有用 的解决办法

热门文章

  1. SFTP多用户权限 linux环境 一站式解决方案
  2. vue控制台报错Duplicate keys detected: 'xxxx'. This may cause an update error.解决方案
  3. mysql 插入数据时 自动设置创建时间和更新时间
  4. 工作流实战_10_flowable 流程实例的删除
  5. 支付宝和微信的JSSDK发起支付
  6. php对帖子分类,php – MySQL:从类别中获取帖子
  7. 天龙八部服务器都需要那种系统,天龙八部排行榜系统怎么看 排行榜系统分类介绍...
  8. c语言偶数分解成两个素数,如何用C语言验证2000以内的哥德巴赫猜想,对于任何大于4的偶数均可以分解为两个素数之和....
  9. 卸载源码安装mysql_CentOS7下源码安装MySQL5.7.6+
  10. tp5 mysql实现消息队列_TP5系列 | Queue消息队列