ConcurrentHashMap 原理分析
1 为什么有ConcurrentHashMap
hashmap是非线程安全的,hashtable是线程安全的,但是所有的写和读方法都有synchronized,所以同一时间只有一个线程可以持有对象,多线程情况下锁竞争会比较激烈,严重影响性能。基于这种情况,Doug Lee大师写了一个ConcurrentHashMap类。ConcurrentHashMap是对多线程各种特性深刻理解的经典范例,学习多线程编程不得不学ConcurrentHashMap。
2 特性
ConcurrentHashMap通过锁拆分机制,降低了锁的争用,写时加锁,读时不加锁,降低了锁的持有时间,所以ConcurrentHashMap在高并发情况下的性能得到了大幅提升,ConcurrentHashMap非常适用于读多写少的场景中。
3 原理
3.1 锁拆分
ConcurrentHashMap引入了Segment,通过将键值做hash,数据可以均匀的分布到每个Segment中,每次put,remove等操作的时候,锁的都是当前的Segment,这样就减少了锁的争用。
4 整体类图和关键数据结构
4.1 类图

4.2 数据存储

代码片段1:
ConcurrentHashMap:
final int segmentMask;
final int segmentShift;
final Segment<K,V>[] segments;
代码片段2:
SegMent:
transient volatile int count;
transient int modCount;
transient int threshold;
transient volatile HashEntry<K,V>[] table;
final float loadFactor;
代码片段3:
HashEntry:
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
由上可见,ConcurrentHashMap由Segment数组组成,Segment由table数组组成,每一个table元素都是一个由HashEntry组成的链表结构,hash冲突时会存放到同一个table的链表结构中。键值对保存在HashEntry对象中。
依次插入A B C后,Segment结构示意图:

4.3 Segment特性
transient volatile HashEntry<K,V>[] table;
是volatile的,避免了读取时加锁,volatile特性约束变量的值在本地线程副本中修改后会立即同步到主线程中,保证了其他线程的可见性。
4.3 HashEntry
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
除value外,其他的属性都是final的,value是volatile类型的,都修饰为final表明不允许在此链表结构的中间或者尾部做添加删除操作,每次只允许操作链表的头部。删除元素后,删除元素之后的链表保持不变,删除元素之前的链表重新复制一份,并指向删除元素之后的元素。
例如删除C元素:

注意删除之后原来元素的顺序反转了。
5 关键点:
5.1 put
ConcurrentHashMap:
public V put(K key, V value) {
//不允许value为空
if (value == null)
throw new NullPointerException();
int hash = hash(key.hashCode());
//通过segmentFor(hash)找到找到数据所在的segment
//调用Segment的put方法完成put操作
return segmentFor(hash).put(key, hash, value, false);
}
Segment:
V put(K key, int hash, V value, boolean onlyIfAbsent) {
//put操作需要先获取锁
lock();
try {
int c = count;
//超出界限,进行rehash,table容量扩充1倍。
if (c++ > threshold) // ensure capacity
rehash();
//找到HashEntry的头
HashEntry<K,V>[] tab = table;
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;
if (e != null) {//如果已经存在,则直接替换value
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {//如果不存在,则插入到表头
oldValue = null;
++modCount;//用于记录链表结构化调整,跨段求size会用到
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
5.2 get
V get(Object key, int hash) {
if (count != 0) { // read-volatile
//获取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;
//因为put的value不允许为空,所以如果值为空,说明有其他线程正在构造hashentry对象,发生了指令重排序,所以加锁重新读取一次。
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
5.3 size
final Segment<K,V>[] segments = this.segments;
long sum = 0;
long check = 0;
int[] mc = new int[segments.length];
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
check = 0;
sum = 0;
int mcsum = 0;
//遍历,并记录下每个Segment的modCount值
for (int i = 0; i < segments.length; ++i) {
sum += segments[i].count;
mcsum += mc[i] = segments[i].modCount;
}
if (mcsum != 0) {
//再遍历一次,看2次是否相同,如果不相同则再试一次,如果相同则返回size.
for (int i = 0; i < segments.length; ++i) {
check += segments[i].count;
if (mc[i] != segments[i].modCount) {
check = -1; // force retry
break;
}
}
}
if (check == sum)
break;
}
//尝试2次后,如果仍然不相等,则加锁重新读一遍。
if (check != sum) { // Resort to locking all segments
sum = 0;
for (int i = 0; i < segments.length; ++i)
segments[i].lock();
for (int i = 0; i < segments.length; ++i)
sum += segments[i].count;
for (int i = 0; i < segments.length; ++i)
segments[i].unlock();
}
if (sum > Integer.MAX_VALUE)
return Integer.MAX_VALUE;
else
return (int)sum;
6. 思考
6.1 为什么查询可以不加锁?
1)通过HashEntry的不变性降低读操作加锁的需求。
HashEntry的属性key,next,hash都是final类型的,保证只能在头部修改链表,另外value设置为了volatile,保证了写线程写入后,其他读线程都可以看到新值。
非结构化修改:对于非结构化修改,因为value是volatile类型的,所以写线程修改后,读线程立刻可以看到修改后的值。
结构化修改:a)put,由于put插入到链表的表头,链表中的原有节点并没有改变,所以读线程可以正常遍历原有的链表
b)remove ,参见4.3中的图,原有链表也继续保留,所以读线程可以正常遍历链表。
2)用volatile变量协调读写线程的可见性

假设线程M写入count后,线程N读取count。
根据happen-before法则,A happen-before B, C happen-before D, 又根据volatile法则,B happen-bofere C,所以根据传递规则A happen-before D。
从get的代码中看,get会首先读取count,所以读线程能够看到之前对链表做的修改。
6.2 什么时候会造成数据不一致?
线程A先做put操作,线程B后做get操作。
假设put执行到红色注释处,切换到线程B则读到的是线程A put之前的操作,这个概率比较小,并且是允许的,如果要保证严格的一致性,那么只有给读操作加锁。这也印证了每种技术都有其适用的场景那句话,ConcurrentHashMap适用在读多写少的场景下。
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
//-------------put执行到此处-----------
V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}
7 参考资料:
http://blog.csdn.net/ykdsg/article/details/6257449
http://bhdweb.iteye.com/blog/1722431
http://bhdweb.iteye.com/blog/1722432
http://www.360doc.com/content/12/1105/20/9462341_246041701.shtml
http://www.gznote.com/2014/04/concurrenthashmap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.html *
http://www.iteye.com/topic/344876
http://andy136566.iteye.com/blog/1070493
ConcurrentHashMap 原理分析的更多相关文章
- ConcurrentHashMap原理分析(1.7与1.8)-put和 get 需要执行两次Hash
ConcurrentHashMap 与HashMap和Hashtable 最大的不同在于:put和 get 两次Hash到达指定的HashEntry,第一次hash到达Segment,第二次到达Seg ...
- [转载] ConcurrentHashMap原理分析
转载自http://blog.csdn.net/liuzhengkang/article/details/2916620 集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的 ...
- Java集合:ConcurrentHashMap原理分析
集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...
- 【Java并发编程】1、ConcurrentHashMap原理分析
集合是编程中最常用的数据结构.而谈到并发,几乎总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap).这篇文章主 ...
- Java 中 ConcurrentHashMap 原理分析
一.Java并发基础 当一个对象或变量可以被多个线程共享的时候,就有可能使得程序的逻辑出现问题. 在一个对象中有一个变量i=0,有两个线程A,B都想对i加1,这个时候便有问题显现出来,关键就是对i加1 ...
- ConcurrentHashMap原理分析(二)-扩容
概述 在上一篇文章中介绍了ConcurrentHashMap的存储结构,以及put和get方法,那本篇文章就介绍一下其扩容原理.其实说到扩容,无非就是新建一个数组,然后把旧的数组中的数据拷贝到新的数组 ...
- ConcurrentHashMap原理分析
当我们享受着jdk带来的便利时同样承受它带来的不幸恶果.通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,安全的背后是巨大的浪费,而现在的解 ...
- ConcurrentHashMap原理分析(1.7与1.8)
前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新 ...
- 【转】ConcurrentHashMap原理分析(1.7与1.8)
https://www.cnblogs.com/study-everyday/p/6430462.html 前言 以前写过介绍HashMap的文章,文中提到过HashMap在put的时候,插入的元素超 ...
随机推荐
- C# 封装
封装就是吧里面实现的细节包起来,这样很复杂的逻辑经过包装之后给别人使用就很方便,别人不需要了解里面是如何实现的,只要传入所需要的参数就可以得到想要的结果.其实这和黑盒测试差不多
- SGU 185.Two shortest (最小费用最大流)
时间限制:0.25s 空间限制:4M 题意: 在n(n<=400)个点的图中,找到并输出两条不想交的最短路.不存在输出“No sulotion”: Solution: 最小费用最大流 建图与po ...
- Xcode编译项目出现访问private key提示框
原因: 在编译时Xcode进行codesign时需要访问"private key"时没有权限,然后让询问是否允许,有三个选项,全部允许.否绝.允许,一次弹出4个(我遇到的) 遇到问 ...
- chromedriver release note
----------ChromeDriver v2.25 (2016-10-25)---------- Supports Chrome v53-55 Resolved issue 1547: Chro ...
- 13号中断 int 13(转)
第一部分 简 介 1,1 一. 硬盘结构简介 1. 硬盘参数释疑 到目前为止, 人们常说的 ...
- Bootstrap_Javascript_选项卡
选项卡Tabs是Web中一种非常常用的功能.用户点击或悬浮对应的菜单项,能切换出对应的内容. 一 . 结构分析 Bootstrap框架中的选项卡主要有两部分内容组成: 选项卡组件(也就是菜单组件),对 ...
- TatukGIS-TGIS_LayerVector-LocateEx
方法原型: function LocateEx(const _ptg: TGIS_Point; const _prec: Double; const _uid: Integer; var _dist: ...
- shell脚本中的标准输出重定向使用涵义
0表示标准输入 1表示标准输出 2表示标准错误输出 > 默认为标准输出重定向,与 1> 相同 2>&1 意思是把 标准错误输出 重定向到 标准输出. &>fil ...
- IOS“多继承”
转自念茜的博客: 当单继承不够用,很难为问题域建模时,我们通常都会直接想到多继承.多继承是从多余一个直接基类派生类的能力,可以更加直接地为应用程序建模.但是Objective-C不支持多继承,由于消息 ...
- JSP技术的优缺点介绍
什么是JSP?JSP可用一种简单易懂的等式表示为:HTML+Java=JSP. JSP技术使用Java编程语言编写类XML的tags和scriptlets,来封装产生动态网页的处理逻辑. 网页还能通过 ...