ConcurrentHashMap实现解析
ConcurrentHashMap是线程安全的HashMap的实现,具有更加高效的并发性。与HashTable不同,ConcurrentHashMap运用锁分离技术,尽量减小写操作时加锁的粒度,即在写操作时,不用对整个ConcurrentHashMap加锁。为了实现,ConcurrentHashMap采用了Segment结构,每个Segment中维护了一个链表数组,在存取操作过程中实现两次哈希。在写数据的过程中,对每个Segment加锁,这样如果操作的数据位于两个不同的Segment中,便可并发进行,大大提高了并发的效率。
HashTable和ConcurrentHashMap在内部结构上的区别:
HashTable: ConcurrentHashMap:
左边便是Hashtable的实现方式---整个Hash表加锁;而右边则是ConcurrentHashMap的实现方式---分段。ConcurrentHashMap默认将hash表分为16个段,诸如get,put,remove等常用操作只锁当前需要用到的段。这样,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制),并发性的提升是显而易见的。以下代码是基于jdk1.5,在jdk1.7中,put操作用了自旋锁的机制,理解起来费劲。
1.segment的数据结构:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
//Segment中元素的数量
transient volatile int count;
//对table的大小造成影响的操作的次数
transient int modCount;
//阈值,Segment里面元素的数量超过这 个值依旧就会对Segment进行扩容
transient int threshold;
//链表数组,每个segment维持一个数组
transient volatile HashEntry<K,V>[] table;
//负载因子
final float loadFactor;
}
2.每个Entry(HashEntry)的结构:
static final class HashEntry<K,V> {
//key-value对的key值
final K key;
final int hash;
//key-value对的value值
volatile V value;
//链表指向下一个Entry的引用
final HashEntry<K,V> next;
}
3.ConcurrentHashMap的初始化
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;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newArray(ssize); if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = 1;
while (cap < c)
cap <<= 1; for (int i = 0; i < this.segments.length; ++i)
this.segments[i] = new Segment<K,V>(cap, loadFactor);
}
有三个参数:
initialCapacity:表示初始的容量;
loadFactor:表示负载因子参数;
concurrentLevel:表示ConcurrentHashMap内部的Segment的数量;
ConcurrentLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样做扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash。Segment的数量是不大于concurrentLevel的最大的2的指数,就是说Segment的数量永远是2的指数个,这样的好处是方便采用移位操作来进行hash(通过按位与的哈希算法来定位segments数组的索引),加快hash的过程;根据intialCapacity确定Segment的容量的大小,每一个Segment的容量大小也是2的指数(同理),同样使为了加快hash的过程。segmentShift和segmentMask,这两个变量在定位segment时的哈希算法里需要使用,很重要。
4.segment内部的put操作:
//返回的是原来已有的和key相同的HashEntry的value值
V put(K key, int hash, V value, boolean onlyIfAbsent){
//加锁
lock();
try {
//当前segment中HashEntry的数量
int c = count;
//需要进行扩容,rehash
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);//定位HashEntry,即在HashEntry Table中的下标
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
//找到所在链表中key值和要加入的key值相同的HashEntry
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 {//未找到,e为null,则新生成一个HashEntry,并将原来的链作为自己的next
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();//释放锁
}
}
4.segment内部的put操作,如上述,不用加锁:
V get(Object key, int hash) {
if (count != 0) { // read-volatile
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;
}
V readValueUnderLock(HashEntry<K,V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
注释://recheck,可能有点费解,v怎么可能会是null呢?在put操作时(不是segment内部的操作,而是整个Hash表的put操作中会判断如果value值为null会抛出异常),空值的唯一源头就是HashEntry中的默认值,因为HashEntry中的value不是final的,非同步读取有可能读取到空值。看下put操作的语句:tab[index] = new HashEntry<K,V>(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,这就可能导致结点的值为空。这种情况应当很罕见,一旦发生这种情况,ConcurrentHashMap采取的方式是在持有锁的情况下再读一遍,这能够保证读到最新的值,并且一定不会为空值。(引)
5.remove操作:
删除操作是加锁的,有多个删除操作同时进行,只要删除的对象不在同一段内,则可以并发执行,大大提高了并发的效率。整个ConcurrentHashMap操作也是借助于在segment上的操作,先将待删除的HashEntry定位到相应的segment,在segment上做删除操作。
V remove(Object key, int hash, Object value) {
lock();
try {
int c = count - 1;
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;
<span style="white-space:pre"> </span>V oldValue = null;
if (e != null) {
V v = e.value;
if (value == null || value.equals(v)) {
oldValue = v;
// All entries following removed node can stay
// in list, but all preceding ones need to be
// cloned.
++modCount;
HashEntry<K,V> newFirst = e.next;
for (HashEntry<K,V> p = first; p != e; p = p.next)
newFirst = new HashEntry<K,V>(p.key, p.hash,
newFirst, p.value);
tab[index] = newFirst;
count = c; // write-volatile
}
}
return oldValue;
} finally {
unlock();
}
}
首先找到待删除的节点,如果不存在这个节点就直接返回null,否则就要将待删除节点(节点e)前面的结点复制一遍,尾结点指向e的下一个结点。将e后面的结点复制,可以重复使用。当要删除的结点存在时,删除的最后一步操作要将count的值减一。这必须是最后一步操作,否则读取操作可能看不到之前对段所做的结构性修改。删除之后,e前面的元素的顺序会发生改变:
6.size()操作:
用于统计ConcurrentHashMap中元素的个数,是跨段操作的。首先在没有加锁的情况下,遍历所有的segment,看得到的所有段的count和和modCount和相同与否,重复计算比较RETRIES_BEFORE_LOCK次,如果相同则代表在统计过程中没有发生remove或put操作,直接返回。如果不相同,则把这个过程再重复做一次。若还不相同,则就需要将所有的Segment都加锁,然后遍历。
public int 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;
for (int i = 0; i < segments.length; ++i) {
sum += segments[i].count;
mcsum += mc[i] = segments[i].modCount;
}
if (mcsum != 0) {
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;
}
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;
}
总结:
ConcurrentHashMap利用了锁分离技术实现了更高性能的并发,实现方式很精妙。关于ConcurrentHashMap的更多内容还要继续学习。
ConcurrentHashMap实现解析的更多相关文章
- Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析 今天发一篇”水文”,可能很多读者都会表示不理解,不过我想把它作为并发序列文章中不可缺少的一块来介绍.本来以为花不了 ...
- Java并发指南13:Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析
Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析 转自https://www.javadoop.com/post/hashmap#toc7 部分内容转自 http: ...
- ConcurrentHashMap源代码解析
这些天一直在看集合相关的源代码.确实学到了不少东西.这些集合都是息息相关的,学了就停不下来! 学集合就必须要学习锁的知识.学了锁那么并发编程的知识也不能少,都是非常重要的基础知识. jdk1.8的源代 ...
- 【转】ConcurrentHashMap完全解析(JDK6/7、JDK8)
转自http://my.oschina.net/hosee/blog/675884 并发编程实践中,ConcurrentHashMap是一个经常被使用的数据结构,相比于Hashtable以及Colle ...
- ConcurrentHashMap完全解析(jdk6/7,8)
并发编程实践中,ConcurrentHashMap是一个经常被使用的数据结构,相比于Hashtable以及Collections.synchronizedMap(),ConcurrentHashMap ...
- Java7 和 Java8 中的 ConcurrentHashMap 原理解析
Java7 中 ConcurrentHashMap ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些. 整个 ConcurrentHash ...
- ConcurrentHashMap代码解析
ConcurrentHashMap (JDK 1.7)的继承关系如下: 1. ConcurrentHashMap是线程安全的hash map.ConcurrentHashMap的数据结构是一个Segm ...
- 并发容器(四)ConcurrentHashMap 深入解析(JDK1.6)
这篇文章深入分析的是 JDK1.6的 ConcurrentHashMap 的实现原理,但在JDK1.8中又改进了 ConcurrentHashMap 的实现,废弃了 segments.虽然是已经被 ...
- java并发编程的艺术(四)---ConcurrentHashMap原理解析
本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...
随机推荐
- c++ 用构造函数
1.c++ 用构造函数创建一个对象时,对象里的属性应该是全新的不存在把之前的属性留下的问题.之所以debug里会存在上一个变量的信息,应该是debug里的一个bug,与程序无光.
- BootStrap中按钮点击后被禁用按钮的最佳实现方法
//禁用button $('button').addClass('disabled'); // Disables visually $('button').prop('disabled', true) ...
- apache+php生产环境错误记录
报错1: [18-Jul-2016 14:36:31 Asia/Shanghai] PHP Warning: DOMDocument::load(): I/O warning : failed to ...
- 查看磁盘读写:iotop
iotop命令用来动态地查看磁盘IO情况,用法如下: [root@localhost ~]$ yum install -y iotop # 安装iotop命令 [root@localhost ~]$ ...
- 安装memcacheq
1.下载memcacheq包 下载地址:http://code.google.com/p/memcacheq/downloads/list 解压包:# tar -zxvf memcache ...
- C++11新特性之七——final/override控制
重载和重写的区别参见: C++继承中重载.重写.重定义的区别: 在了解C++11中的final/override关键字之前,我们先回顾一下C++关于重载的概念.简单地说,一个类A中声明的虚函数fun在 ...
- Jquery checkbox选中问题
checkbox中有.checked的写法,判断当前是否是选中状态,不过这种是针对[object HTMLInputElement]这种类型的,而对于[object Object]这种类型是不能使用的 ...
- c++11——move/forward
std::move c++11中提供了std::move()来将左值转换为右值,从而方便的使用移动语义.move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝. c ...
- BNU4208:Bubble sort
冒泡排序(BubbleSort)的基本概念是:依次比较相邻的两个数,将小数放在前面,大数放在后面.即首先比较第1个和第2个数,将小数放前,大数放后.然后比较第2个数和第3个数,将小数放前,大数放后,如 ...
- Day05_C操作符及二进制补码计算
回顾: 1.数据类型 2.二进制(八进制,十六进制) --------------------------------------------------------- 计算机中不可以使用负号表示 ...