前言

前段时间把 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. java基础77 Http协议及Servlet中的GET、POST提交方式

    本文知识点(目录): 1.什么是http协议    2.查看http协议的工具    3.http协议的内容    4.请求方式    5.请求头和响应头(以及获取请求头信息的方法)    6.实体内 ...

  2. java基础69 JavaScript产生伪验证码(网页知识)

    1.伪验证码 <!doctype html> //软件版本:DW2018版 <html> <head> <meta charset="utf-8&q ...

  3. Oracle常用sql语句。

    最近工作中遇到了一些与oracle相关的问题,稍微整理一些工作中用到的SQL语句 时间相关 --查询距当前时间十分钟内的数据 select sysdate -interval '10' minute ...

  4. 将DataTable转换为List,将List转换为DataTable的实现类

    将DataTable转换为List,将List转换为DataTable的实现类 public static class DataTableHelper { public static DataTabl ...

  5. 程序设计实习MOOC / 程序设计与算法(一)第二周测验(2018春季)

    编程题: 1:对齐输出 总时间限制:  1000ms 内存限制:  65536kB 描述 读入三个整数,按每个整数占8个字符的宽度,右对齐输出它们. 输入 只有一行,包含三个整数,整数之间以一个空格分 ...

  6. 【51nod】1164 最高的奖励 V2

    题解 一道比较神奇的二分图匹配 既然有n个元素,那么能匹配n个位置,我们把这n个位置找出来,是每个区间从左端点开始找到一个没有被匹配到的位置作为该点(我们忽略右端点) 然后我们从价值大到小,然后从左端 ...

  7. Linux 的软件安装目录

    Linux 的软件安装目录是也是有讲究的,理解这一点,在对系统管理是有益的 /usr:系统级的目录,可以理解为C:/Windows/,/usr/lib理解为C:/Windows/System32. / ...

  8. Django实战(15):Django实现RESTful web service

    曾几何时,Ajax已经统治了Web开发中的客户端,而REST成为web世界中最流行的架构风格(architecture style).所以我们的选择变得很简单:前端ajax访问后端的RESTful w ...

  9. const理解

    const int * a4 = &a1; ///const data,non-const pointer int * const a5 = &a1; ///non-const dat ...

  10. Tuning Optimization

    2017-02-22 在经过Trace的分析并重新优化之后 CPU: Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.6GHz 24core 如上图,CPU使用率平均低于60% ...