java中ConcurrentHashMap的使用及在Java 8中的冲突方案

java中ConcurrentHashMap的使用及在Java 8中的冲突方案

一、引言

ConcurrentHashMap(简称CHM)是在Java 1.5做为Hashtable的替代选择新引入的,是concurrent包的重要成员。在Java 1.5以前,若是想要实现一个能够在多线程和并发的程序中安全使用的Map,只能在HashTable和synchronized Map中选择,由于HashMap并非线程安全的。但再引入了CHM以后,咱们有了更好的选择。CHM不可是线程安全的,并且比HashTable和synchronizedMap的性能要好。相对于HashTable和synchronizedMap锁住了整个Map,CHM只锁住部分Map。CHM容许并发的读操做,同时经过同步锁在写操做时保持数据完整性。在这篇博客中我将介绍如下几点:html

  • CHM在Java中如何实现的java

  • 什么状况下应该使用CHM数组

  • 在Java中使用CHM的例子安全

  • CHM的一些重要特性多线程

二、Java中ConcurrentHashMap的实现

CHM引入了分割,并提供了HashTable支持的全部的功能。在CHM中,支持多线程对Map作读操做,而且不须要任何的blocking。这得益于CHM将Map分割成了不一样的部分,在执行更新操做时只锁住一部分。根据默认的并发级别(concurrency level),Map被分割成16个部分,而且由不一样的锁控制。这意味着,同时最多能够有16个写线程操做Map。试想一下,由只能一个线程进入变成同时可由16个写线程同时进入(读线程几乎不受限制),性能的提高是显而易见的。但因为一些更新操做,如put(),remove(),putAll(),clear()只锁住操做的部分,因此在检索操做不能保证返回的是最新的结果。并发

另外一个重要点是在迭代遍历CHM时,keySet返回的iterator是弱一致和fail-safe的,可能不会返回某些最近的改变,而且在遍历过程当中,若是已经遍历的数组上的内容变化了,不会抛出ConcurrentModificationExceptoin的异常。函数

CHM默认的并发级别是16,但能够在建立CHM时经过构造函数改变。毫无疑问,并发级别表明着并发执行更新操做的数目,因此若是只有不多的线程会更新Map,那么建议设置一个低的并发级别。另外,CHM还使用了ReentrantLock来对segments加锁。性能

三、Java中ConcurrentHashMap putifAbsent方法的例子

不少时候咱们但愿在元素不存在时插入元素,咱们通常会像下面那样写代码优化

synchronized(map){
  if (map.get(key) == null){
  return map.put(key, value);
  } else{
  return map.get(key);
  }
}

上面这段代码在HashMap和HashTable中是好用的,但在CHM中是有出错的风险的。这是由于CHM在put操做时并无对整个Map加锁,因此一个线程正在put(k,v)的时候,另外一个线程调用get(k)会获得null,这就会形成一个线程put的值会被另外一个线程put的值所覆盖。固然,你能够将代码封装到synchronized代码块中,这样虽然线程安全了,但会使你的代码变成了单线程。CHM提供的putIfAbsent(key,value)方法原子性的实现了一样的功能,同时避免了上面的线程竞争的风险。线程

四、何时使用ConcurrentHashMap

CHM适用于读者数量超过写者时,当写者数量大于等于读者时,CHM的性能是低于Hashtable和synchronized Map的。这是由于当锁住了整个Map时,读操做要等待对同一部分执行写操做的线程结束。CHM适用于作cache,在程序启动时初始化,以后能够被多个请求线程访问。正如Javadoc说明的那样,CHM是HashTable一个很好的替代,但要记住,CHM的比HashTable的同步性稍弱。

五、使用小结

如今咱们知道了什么是ConcurrentHashMap和何时该用ConcurrentHashMap,下面咱们来复习一下CHM的一些关键点。

  • CHM容许并发的读和线程安全的更新操做

  • 在执行写操做时,CHM只锁住部分的Map

  • 并发的更新是经过内部根据并发级别将Map分割成小部分实现的

  • 高的并发级别会形成时间和空间的浪费,低的并发级别在写线程多时会引发线程间的竞争

  • CHM的全部操做都是线程安全

  • CHM返回的迭代器是弱一致性,fail-safe而且不会抛出ConcurrentModificationException异常

  • CHM不容许null的键值

  • 可使用CHM代替HashTable,但要记住CHM不会锁住整个Map

以上就是Java中CHM的实现和使用场景,下面作进一步深刻探究。

六、冲突解决方案

在Java 8 以前,HashMap和其余基于map的类都是经过链地址法解决冲突,它们使用单向链表来存储相同索引值的元素。在最坏的状况下,这种方式会将HashMap的get方法的性能从O(1)下降到O(n)。为了解决在频繁冲突时hashmap性能下降的问题,Java 8中使用平衡树来替代链表存储冲突的元素。这意味着咱们能够将最坏状况下的性能从O(n)提升到O(logn)

在Java 8中使用常量TREEIFY_THRESHOLD来控制是否切换到平衡树来存储。目前,这个常量值是8,这意味着当有超过8个元素的索引同样时,HashMap会使用树来存储它们。

这一改变是为了继续优化经常使用类。你们可能还记得在Java 7中为了优化经常使用类对ArrayList和HashMap采用了延迟加载的机制,在有元素加入以前不会分配内存,这会减小空的链表和HashMap占用的内存。
这一动态的特性使得HashMap一开始使用链表,并在冲突的元素数量超过指定值时用平衡二叉树替换链表。不过这一特性在全部基于hash table的类中并无,例如Hashtable和WeakHashMap。

目前,只有ConcurrentHashMap,LinkedHashMap和HashMap会在频繁冲突的状况下使用平衡树。

七、何时会产生冲突

HashMap中调用hashCode()方法来计算hashCode。
因为在Java中两个不一样的对象可能有同样的hashCode,因此不一样的键可能有同样hashCode,从而致使冲突的产生。

八、冲突解决小结

  • HashMap在处理冲突时使用链表存储相同索引的元素。

  • 从Java 8开始,HashMap,ConcurrentHashMap和LinkedHashMap在处理频繁冲突时将使用平衡树来代替链表,当同一hash桶中的元素数量超过特定的值便会由链表切换到平衡树,这会将get()方法的性能从O(n)提升到O(logn)。

  • 当从链表切换到平衡树时,HashMap迭代的顺序将会改变。不过这并不会形成什么问题,由于HashMap并无对迭代的顺序提供任何保证。

  • 从Java 1中就存在的Hashtable类为了保证迭代顺序不变,即使在频繁冲突的状况下也不会使用平衡树。这一决定是为了避免破坏某些较老的须要依赖于Hashtable迭代顺序的Java应用。

  • 除了Hashtable以外,WeakHashMap和IdentityHashMap也不会在频繁冲突的状况下使用平衡树。

  • 使用HashMap之因此会产生冲突是由于使用了键对象的hashCode()方法,而equals()和hashCode()方法不保证不一样对象的hashCode是不一样的。须要记住的是,相同对象的hashCode必定是相同的,但相同的hashCode不必定是相同的对象。

  • 在HashTable和HashMap中,冲突的产生是因为不一样对象的hashCode()方法返回了同样的值。
    以上就是Java中HashMap如何处理冲突。这种方法被称为链地址法,由于使用链表存储同一桶内的元素。一般状况HashMap,HashSet,LinkedHashSet,LinkedHashMap,ConcurrentHashMap,HashTable,IdentityHashMap和WeakHashMap均采用这种方法处理冲突。

从JDK 8开始,HashMap,LinkedHashMap和ConcurrentHashMap为了提高性能,在频繁冲突的时候使用平衡树来替代链表。由于HashSet内部使用了HashMap,LinkedHashSet内部使用了LinkedHashMap,因此他们的性能也会获得提高。

http://javarevisited.blogspot.com/2013/02/concurrenthashmap-in-java-example-tutorial-working.html
http://javarevisited.blogspot.jp/2016/01/how-does-java-hashmap-or-linkedhahsmap-handles.html

相关文章
相关标签/搜索