前言

前段时间把 JDK 1.6 中的 HashMap 主要的一些操作源码分析了一次。既然把 HashMap 源码分析了, 就顺便把 JDK 1.6 中 ConcurrentHashMap 的主要一些操作源码分析一下。因为其中有很多思想是值得我们去借鉴的。 ConcurrentHashMap 中的分段锁。这个思想在 JDK 1.8 中 为了优化 JUC 下的原子锁 CAS 高并发情况下导致自旋次数太多效率低下。引用 Adder 。其中就是借鉴了分段锁的思想。AtomicLong 对比 LongAdder。 有兴趣可以查看。

准备

如果有人问你了解 ConcurrentHashMap 吗? 你可以这样回答,了解。 ConcurrentHashMap 是为了 取代 HashMap非线程安全的,一种线程安全实现类。它有一个 Segment 数组,Segment 本身就是相当于一个 HashMap 对象。里面是一个 HashEntry 数组,数组中的每一个 HashEntry 都是一个键值对,也是一个链表的表头。如果别人问你,那 ConcurrentHashMap get 或者 put 一个对象的时候是怎么操作的 ,你该怎么回答。emmm..... 继续往下看。会有你要的答案。

构造函数

分析源码,先从构造函数开始。直接研究带所有参数的构造方法,其他一些重载的构造方法,最里面还是调用了该构造方法。在看构造方法之前,需要 明白 sshift 是表示并发数的2的几次方 比如并发数是16 那么他的值就是 4 。ssize 是 segment 数组的大小。

 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;
}
// 用来与 key的hashCode >>> 运算 获取HashCode的高位
segmentShift = 32 - sshift;
// 高位与 它做与运算 eg 假如 默认的创建该对象 那么 segmentShift = 28 segmentMask=15(二进制为1111) 假如现在put一个值 他的key的HashCode值为2的32次方 那么 他在segment里面的定位时 2的32次方 无符号 高位补零 右移28个 那么就等于 10000(二进制) 等于 16 与 1111 做与运算 等于0 也就是定位在 segment[0]上 。
segmentMask = ssize - 1; // segment数组大小为 16
this.segments = Segment.newArray(ssize); if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// segment数组中 每个HashEntry数组的大小,
int cap = 1;
while (cap < c)
cap <<= 1;
// 为segment数组中的每个HashEntry数组初始化大小,每个semengt中只有一个HashEntry数组。如果你设置的 ConcurrentHashMap 初始化大小为16的话,则 segment数组中每个的HashEntry的大小为1,如果你初始化他的大小为28 的话。它会根据上面的运算,cap的大小为2,也就是segment数组中的每个HashEntry数组的大小为2 ,总的大小为32。
for (int i = 0; i < this.segments.length; ++i)
this.segments[i] = new Segment<K,V>(cap, loadFactor);
}

上面的注释应该都挺清楚了,要注意的是 ConcurrentHashMap的大小 是所有 Segment 数组中每个HashEntry数组的大小相加的和。

put 方法

ConcurrentHashMap 每次 put 的时候都是需要加锁的,只不过会锁住他所在的那个Segment数组位置。其他的不锁,这也就是分段锁,默认支持16个并发。说起put,以数组的形式存储的数据,就会涉及到扩容。这样是接下来需要好好讨论的一个事情。

 	public V put(K key, V value) {
// key value 不能为null
if (value == null)
throw new NullPointerException();
// 获取hash值
int hash = hash(key.hashCode());
// 先获取hash二进制数的高位与15的二进制做与运算,得到segment数组的位置。
return segmentFor(hash).put(key, hash, value, false);
} V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 锁住
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
// 扩容操作
rehash();
// 获取 Segment数组中的其中的HashEntry数组
HashEntry<K,V>[] tab = table;
// 获取在在HashEntry数组中的位置。
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
// 判断是否是该key。
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next; V oldValue;
// 如果存在该key的数据 ,那么更新该值 返回旧值
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
//头插法插入 tab[index]
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
} // 看下扩容操作的细节
void rehash() {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity >= MAXIMUM_CAPACITY)
return; // HashEntry数组,新的数组为它的两倍
HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1);
// 阈值
threshold = (int)(newTable.length * loadFactor);
//他的二进制添加以为 原来他的大小为3 那么二进制就是11 现在就为 7 二进制为 111
int sizeMask = newTable.length - 1;
for (int i = 0; i < oldCapacity ; i++) { // 旧的HashEntry。
HashEntry<K,V> e = oldTable[i]; if (e != null) {
// 下一个 该HashEntry数组上的 HashEntry是否为链表,有下一个值。
HashEntry<K,V> next = e.next;
// 该HashEntry的新位置 如果高位为1 那么他在HashEntry数组中的位置就是老的HashEntry数组中的加上这个倍数。举个例子
// 假如e.hash 原来的的二进制位...111 老的HashEntry数组的大小为 4 那么e.hash和 4-1 也就是3 做与运算 得到的值也就是二进制的11
// 值位3 现在新的HashEntry数组的大小为 8 也就是 e.hash 和 8-1 做与运算 得到的值 也就是二进制位 111 位 7 。
int idx = e.hash & sizeMask; // 没有的话就直接放入该位置了,如果有的话往下看:
if (next == null)
newTable[idx] = e; else {
HashEntry<K,V> lastRun = e;
// 假如idx 等于 7
int lastIdx = idx;
// 找到最后一个 HashEntry中的位置,并且后面的HashEntry的位置都是一样的。举个例子
// 假如这个链表中的所有HashEntry的Hash值为 1-5-1-5-5-5 。那么最后lastIdx = 5 也就是1-5-1后面的这个5 。lastRun 为 1-5-1后面的这个5的HashEnrty。
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
//
lastRun = last;
}
}
// 将 lastRun 复制给 这个新的Table 那么后面还有 5-5-5 这些的就不用移动了 直接带过来了。 这就是上面那个for循环做的事情
newTable[lastIdx] = lastRun; // 对前面的 1-5-1做操作了 1就是在新HashEntry书中的1的位置 5的后就是头插法 ,查到新HashEntry的头部了
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
int k = p.hash & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(p.key, p.hash,
n, p.value);
}
}
}
}
table = newTable;
}

其实put 方法中有点难理解的就是 把查找到后面如果有所有相同的 HashEntry key 的位置是一样的话,就不用额外的进行Hash重新定位了。不知道我描述的清不清楚。如果还有不清楚的话,可以私信一下我。

get 方法

ConcurrentHashMapget 方法是不会加锁的,如果get的值为null的时候,这个时候会对这个HashEntry进行加锁。预防此时并发出现的问题。

    public V get(Object key) {
//定位Segment数组中的HashEntry数组为位置
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
} V get(Object key, int hash) {
// 曾经put进去过,也就是里面有值
if (count != 0) { // read-volatile
// 定位HashEntry数组中的HashEntry。
HashEntry<K,V> e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}

ConcurrentHashMapget方法是比较简单的。看一看就知道了。

总结

这一遍ConcurrentHashMap源码分析,可以说是自己写了大半个月吧。好早之前就准备写了。总是写一点,然后就停笔了。加上自己换了公司的原因,又忙上了一段时间,导致一拖再落。哇,严重拖延症患者。上面自己也是全部透彻之后写下来的,如果有些表达不够清晰的还得多加包涵,如果有不同的可以下方浏览讨论一下。上面很多关键的代码我都写上了注释,可以配合着注释,然后自己对源码进行研究,查看,如果还有不是很透彻的话,自己多翻一翻其他人写的。最近一直在写LeetCode上的动态规划这些算法题。其实也就是抄一遍。等以后有了感悟再来写这一些吧。

ConcurrentHashMap JDK 1.6 源码分析的更多相关文章

  1. HashMap和ConcurrentHashMap实现原理及源码分析

    HashMap实现原理及源码分析 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表, ...

  2. ConcurrentHashMap实现原理及源码分析

    ConcurrentHashMap实现原理 ConcurrentHashMap源码分析 总结 ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对Ha ...

  3. ConcurrentHashMap实现原理以及源码分析

    ConcurrentHashMap是HashMap的高并发版本,是线程安全的,而HashMap是非线程安全的 一.底层实现 底层结构跟hashmap一样,都是通过数组+链表+红黑树实现的,不过它要保证 ...

  4. 还不懂 ConcurrentHashMap ?这份源码分析了解一下

    上一篇文章介绍了 HashMap 源码,反响不错,也有很多同学发表了自己的观点,这次又来了,这次是 ConcurrentHashMap 了,作为线程安全的HashMap ,它的使用频率也是很高.那么它 ...

  5. JDK集合框架源码分析 - 简单概要

    1.类继承体系 在集合框架的类继承体系中,最顶层有两个接口Collection.Map: Collection 表示一组纯数据 Map 表示一组key-value对 Collection的类继承体系: ...

  6. 观察者模式—jdk自带源码分析

    一:观察者模式简介 二:jdk实现观察者模式的源码 三:实际例子 四:观察者模式的优点和不足 五:总结 一:观察者模式简介 有时又被称为发布(publish )-订阅(Subscribe)模式.模型- ...

  7. 2.Java集合-ConcurrentHashMap实现原理及源码分析

    一.为何用ConcurrentHashMap 在并发编程中使用HashMap可能会导致死循环,而使用线程安全的HashTable效率又低下. 线程不安全的HashMap 在多线程环境下,使用HashM ...

  8. ConcurrentHashMap(Java8)源码分析

    1. 常量.成员变量 private static final int MAXIMUM_CAPACITY = 1 << 30; // 和HashMap一样 private static f ...

  9. 并发-ConcurrentHashMap源码分析

    ConcurrentHashMap 参考: http://www.cnblogs.com/chengxiao/p/6842045.html https://my.oschina.net/hosee/b ...

随机推荐

  1. Ibatis.Net 各种配置说明学习(二)

    1.各个配置文件的配置说明 providers.config:指定数据库提供者,.Net版本等信息. xxxxx.xml:映射规则. SqlMap.config:大部分配置一般都在这里,如数据库连接等 ...

  2. Oracle 函数 “判断数据表中不存在的数据,才允许通过”

    create or replace function mca_detail_material_val(p_material_code VARCHAR2, --实参 p_material_name VA ...

  3. P2448 无尽的生命

    Description 小 a有一个长度无限长的序列 p = (1, 2, 3, 4 --),初始时 pi = i 给出 m 个操作,每次交换两个位置的数 询问最后序列逆序对的个数 Solution ...

  4. AdvStringGrid 复选框、goRowSelect

    var I: Integer; begin do begin AdvStringGrid1.AddCheckBox(, I, True, True); AdvStringGrid1.Cells[,I] ...

  5. Centos之其他帮助命令

    选项帮助 命令 -help 获取命令选项的帮助 例如 ls --help 我们会发现用这种方式查看帮助命令 居然还有中文解释: 详细命令帮助info info 命令 -回车:进入子帮助页面(带有*号标 ...

  6. 解决Url带中文参数乱码问题

    这里我来介绍下如何配置Tomcat 来解决Url带中文参数乱码问题: 首先打开Tomcat安装目录,以Tomcat7为例,其他版本基本一样: 打开conf文件 打开server.xml 大概在70行左 ...

  7. 如何在VS2013创建WebService并在IIS中发布

    第一步:打开VS2013,选择文件->新建->项目. 第二步:选择[ASP.net 空web应用程序],将其命名为自己想的工程名称. 第三步:然后右键点击工程,添加->web服务.然 ...

  8. python类型学习

    python类型学习 标准类型 数字 Integer 整型 Boolean 布尔型 Long integer 长整型 Floating point real numer  浮点型 Complex nu ...

  9. 如何验证一个地址可否使用—— MmIsAddressValid函数分析

    又是一篇内核函数分析的博文,我个人觉得Windows的内核是最好的老师,当你想实现一个功能之前可以看看Windows内核是怎么做的,说不定就有灵感呢:) 首先看下官方的注释说明: /*++ Routi ...

  10. 浅谈ABP最佳实践

    目录 ABP概念简述 ABP在[事务操作]上的简便性 ABP在[关联查询]上的“美”和“坑” ABP的[参数验证]方式 ABP概念简述 ABP是“ASP.NET Boilerplate Project ...