专用于高并发的map类-----Map的并发处理(ConcurrentHashMap)

oncurrentModificationExceptionjava

在这种迭代方式中,当iterator被建立后集合再发生改变就再也不是抛出ConcurrentModificationException, 取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可使用原来老的数据,而写线程也能够并发的完成改变。node

 

ConcurrentHashMap 原理:c++

集合是编程中最经常使用的数据结构。而谈到并发,几乎老是离不开集合这类高级数据结构的支持。好比两个线程须要同时访问一个中间临界区 (Queue),好比常会用缓存做为外部文件的副本(HashMap)。这篇文章主要分析jdk1.5的3种并发集合类型 (concurrent,copyonright,queue)中的ConcurrentHashMap,让咱们从原理上细致的了解它们,可以让咱们在深 度项目开发中获益非浅。编程

    在tiger以前,咱们使用得最多的数据结构之一就是HashMap和Hashtable。你们都知道,HashMap中未进行同步考虑,而 Hashtable则使用了synchronized,带来的直接影响就是可选择,咱们能够在单线程时使用HashMap提升效率,而多线程时用 Hashtable来保证安全。
    当咱们享受着jdk带来的便利时一样承受它带来的不幸恶果。经过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次 锁住整张表让线程独占,安全的背后是巨大的浪费,慧眼独具的Doug Lee立马拿出了解决方案----ConcurrentHashMap。
    ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁。如图




    左边即是Hashtable的实现方式---锁整个hash表;而右边则是ConcurrentHashMap的实现方式---锁桶(或段)。 ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等经常使用操做只锁当前须要用到的桶。试想,原来 只能一个线程进入,如今却能同时16个写线程进入(写线程才须要锁定,而读线程几乎不受限制,以后会提到),并发性的提高是显而易见的。
    更使人惊讶的是ConcurrentHashMap的读取并发,由于在读取的大多数时候都没有用到锁定,因此读取操做几乎是彻底的并发操做,而写操做锁定 的粒度又很是细,比起以前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操做时才须要锁定整个表。而在迭代 时,ConcurrentHashMap使用了不一样于传统集合的快速失败迭代器(见以前的文章《JAVA API备忘---集合》)的另外一种迭代方式,咱们称为弱一致迭代器。在这种迭代方式中,当iterator被建立后集合再发生改变就再也不是抛出 ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再 将头指针替换为新的数据,这样iterator线程可使用原来老的数据,而写线程也能够并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和 扩展性,是性能提高的关键。
    接下来,让咱们看看ConcurrentHashMap中的几个重要方法,内心知道了实现机制后,使用起来就更加有底气。
    ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点),对应上面的图能够看出之间的关系。
    get方法(请注意,这里分析的方法都是针对桶的,由于ConcurrentHashMap的最大改进就是将粒度细化到了桶上),首先判断了当前桶的数据 个数是否为0,为0天然不可能get到什么,只有返回null,这样作避免了没必要要的搜索,也用最小的代价避免出错。而后获得头节点(方法将在下面涉及) 以后就是根据hash和key逐个判断是不是指定的值,若是是而且值非空就说明找到了,直接返回;程序很是简单,但有一个使人困惑的地方,这句 return readValueUnderLock(e)究竟是用来干什么的呢?研究它的代码,在锁定以后返回一个值。但这里已经有一句V v = e.value获得了节点的值,这句return readValueUnderLock(e)是否画蛇添足?事实上,这里彻底是为了并发考虑的,这里当v为空时,多是一个线程正在改变节点,而以前的 get操做都未进行锁定,根据bernstein条件,读后写或写后读都会引发数据的不一致,因此这里要对这个e从新上锁再读一遍,以保证获得的是正确 值,这里不得不佩服Doug Lee思惟的严密性。整个get操做只有不多的状况会锁定,相对于以前的Hashtable,并发是不可避免的啊!

 

Java代码   收藏代码
  1. V get(Object key, int hash) {  
  2.     if (count != 0) { // read-volatile  
  3.         HashEntry e = getFirst(hash);  
  4.         while (e != null) {  
  5.             if (e.hash == hash && key.equals(e.key)) {  
  6.                 V v = e.value;  
  7.                 if (v != null)  
  8.                     return v;  
  9.                 return readValueUnderLock(e); // recheck  
  10.             }  
  11.             e = e.next;  
  12.         }  
  13.     }  
  14.     return null;  
  15. }  
  16.   
  17.   
  18. V readValueUnderLock(HashEntry e) {  
  19.     lock();  
  20.     try {  
  21.         return e.value;  
  22.     } finally {  
  23.         unlock();  
  24.     }  
  25. }  

 

put操做一上来就锁定了整个segment,这固然是为了并发的安全,修改数据是不能并发进行的,必须得有个判断是否超限的语句以确保容量不足时可以 rehash,而比较难懂的是这句int index = hash & (tab.length - 1),原来segment里面才是真正的hashtable,即每一个segment是一个传统意义上的hashtable,如上图,从二者的结构就能够看 出区别,这里就是找出须要的entry在table的哪个位置,以后获得的entry就是这个链的第一个节点,若是e!=null,说明找到了,这是就 要替换节点的值(onlyIfAbsent == false),不然,咱们须要new一个entry,它的后继是first,而让tab[index]指向它,什么意思呢?实际上就是将这个新entry 插入到链头,剩下的就很是容易理解了。缓存

 

Java代码   收藏代码
  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {  
  2.     lock();  
  3.     try {  
  4.         int c = count;  
  5.         if (c++ > threshold) // ensure capacity  
  6.             rehash();  
  7.         HashEntry[] tab = table;  
  8.         int index = hash & (tab.length - 1);  
  9.         HashEntry first = (HashEntry) tab[index];  
  10.         HashEntry e = first;  
  11.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  12.             e = e.next;  
  13.   
  14.         V oldValue;  
  15.         if (e != null) {  
  16.             oldValue = e.value;  
  17.             if (!onlyIfAbsent)  
  18.                 e.value = value;  
  19.         }  
  20.         else {  
  21.             oldValue = null;  
  22.             ++modCount;  
  23.             tab[index] = new HashEntry(key, hash, first, value);  
  24.             count = c; // write-volatile  
  25.         }  
  26.         return oldValue;  
  27.     } finally {  
  28.         unlock();  
  29.     }  
  30. }  

 

 

   remove操做很是相似put,但要注意一点区别,中间那个for循环是作什么用的呢?(*号标记)从代码来看,就是将定位以后的全部entry克隆并 拼回前面去,但有必要吗?每次删除一个元素就要将那以前的元素克隆一遍?这点实际上是由entry 的不变性来决定的,仔细观察entry定义,发现除了value,其余全部属性都是用final来修饰的,这意味着在第一次设置了next域以后便不能再 改变它,取而代之的是将它以前的节点全都克隆一次。至于entry为何要设置为不变性,这跟不变性的访问不须要同步从而节省时间有关,关于不变性的更多 内容,请参阅以前的文章《线程高级---线程的一些编程技巧》安全

 

Java代码   收藏代码
  1. V remove(Object key, int hash, Object value) {  
  2.     lock();  
  3.     try {  
  4.         int c = count - 1;  
  5.         HashEntry[] tab = table;  
  6.         int index = hash & (tab.length - 1);  
  7.         HashEntry first = (HashEntry)tab[index];  
  8.         HashEntry e = first;  
  9.         while (e != null && (e.hash != hash || !key.equals(e.key)))  
  10.             e = e.next;  
  11.   
  12.         V oldValue = null;  
  13.         if (e != null) {  
  14.             V v = e.value;  
  15.             if (value == null || value.equals(v)) {  
  16.                 oldValue = v;  
  17.                 // All entries following removed node can stay  
  18.                 // in list, but all preceding ones need to be  
  19.                 // cloned.  
  20.                 ++modCount;  
  21.                 HashEntry newFirst = e.next;  
  22.             *    for (HashEntry p = first; p != e; p = p.next)  
  23.             *        newFirst = new HashEntry(p.key, p.hash,   
  24.                                                   newFirst, p.value);  
  25.                 tab[index] = newFirst;  
  26.                 count = c; // write-volatile  
  27.             }  
  28.         }  
  29.         return oldValue;  
  30.     } finally {  
  31.         unlock();  
  32.     }  
  33. }  

 

Java代码   收藏代码
  1. static final class HashEntry {  
  2.     final K key;  
  3.     final int hash;  
  4.     volatile V value;  
  5.     final HashEntry next;  
  6.   
  7.     HashEntry(K key, int hash, HashEntry next, V value) {  
  8.         this.key = key;  
  9.         this.hash = hash;  
  10.         this.next = next;  
  11.         this.value = value;  
  12.     }  
  13. }  

 

 

 

 

 

ConcurrentHashMap 和 HashTable 的速度比较:
服务器

util.concurrent 包中的 ConcurrentHashMap 类(也将出如今JDK 1.5中的 java.util.concurrent 包中)是对 Map 的线程安全的实现,比起 synchronizedMap 来,它提供了好得多的并发性。多个读操做几乎总能够并发地执行,同时进行的读和写操做一般也能并发地执行,而同时进行的写操做仍然能够不时地并发进行(相关的类也提供了相似的多个读线程的并发性,可是,只容许有一个活动的写线程) 。ConcurrentHashMap 被设计用来优化检索操做;实际上,成功的 get() 操做完成以后一般根本不会有锁着的资源。要在不使用锁的状况下取得线程安全性须要必定的技巧性,而且须要对Java内存模型(Java Memory Model)的细节有深刻的理解。ConcurrentHashMap 实现,加上 util.concurrent 包的其余部分,已经被研究正确性和线程安全性的并发专家所正视。在下个月的文章中,咱们将看看 ConcurrentHashMap 的实现的细节。数据结构

ConcurrentHashMap 经过稍微地松弛它对调用者的承诺而得到了更高的并发性。检索操做将能够返回由最近完成的插入操做所插入的值,也能够返回在步调上是并发的插入操做所添加的值(可是决不会返回一个没有意义的结果)。由 ConcurrentHashMap.iterator() 返回的 Iterators 将每次最多返回一个元素,而且决不会抛出ConcurrentModificationException 异常,可是可能会也可能不会反映在该迭代器被构建以后发生的插入操做或者移除操做。在对 集合进行迭代时,不须要表范围的锁就能提供线程安全性。在任何不依赖于锁整个表来防止更新的应用程序中,可使用 ConcurrentHashMap 来替代 synchronizedMap 或 Hashtable 。多线程

上述改进使得 ConcurrentHashMap 可以提供比 Hashtable 高得多的可伸缩性,并且,对于不少类型的公用案例(好比共享的cache)来讲,还不用损失其效率。并发

好了多少?

表 1对 Hashtable 和  ConcurrentHashMap 的可伸缩性进行了粗略的比较。在每次运行过程当中, n 个线程并发地执行一个死循环,在这个死循环中这些线程从一个 Hashtable 或者 ConcurrentHashMap 中检索随机的key value,发如今执行 put() 操做时有80%的检索失败率,在执行操做时有1%的检索成功率。测试所在的平台是一个双处理器的Xeon系统,操做系统是Linux。数据显示了10,000,000次迭代以毫秒计的运行时间,这个数据是在将对 ConcurrentHashMap的 操做标准化为一个线程的状况下进行统计的。您能够看到,当线程增长到多个时,ConcurrentHashMap 的性能仍然保持上升趋势,而 Hashtable 的性能则随着争用锁的状况的出现而当即降了下来。

比起一般状况下的服务器应用,此次测试中线程的数量看上去有点少。然而,由于每一个线程都在不停地对表进行操做,因此这与实际环境下使用这个表的更多数量的线程的争用状况基本等同。

表 1.Hashtable 与 ConcurrentHashMap在可伸缩性方面的比较

线程数 ConcurrentHashMap Hashtable
1 1.00 1.03
2 2.59 32.40
4 5.58 78.23
8 13.21 163.48
16 27.58 341.21
32 57.27 778.41
相关文章
相关标签/搜索