HashMap和ConcurrentHashMap分享

来源:互联网 发布:家用加湿器推荐 知乎 编辑:程序博客网 时间:2024/05/16 06:17

原文链接:http://qicen.iteye.com/blog/1913168


大家一看到这两个类就能想到HashMap不是线程安全的,ConcurrentHashMap是线程安全的。除了这些,还知道什么呢? 

先看一下简单的类图: 

从类图中可以看出来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。 
ConcurrentHashMap是使用了锁分段技术技术来保证线程安全的。 
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 

属性说明: 
我们会发现HashMap和Segment里的属性值基本是一样的,因为Segment的本质上就是一个加锁的HashMap,下面是每个属性的意义: 
table:数据存储区 
size,count: 已存数据的大小 
threshold:table需要扩容的临界值,等于table的大小*loadFactor 
loadFactor: 装载因子 
modCount: table结构别修改的次数 

hash算法和table数组长度: 
仔细阅读HashMap的构造方法的话,会发现他做了一个操作保证table数组的大小是2的n次方。 
如果使用new HashMap(10)新建一个HashMap,你会发现这个HashMap中table数组实际的大小是16,并不是10.
为什么要这么做呢?这就要从HashMap里的hash和indexFor方法开始说了。 

Java代码  收藏代码
  1. static int hash(int h) {  
  2.     // This function ensures that hashCodes that differ only by  
  3.     // constant multiples at each bit position have a bounded  
  4.     // number of collisions (approximately 8 at default load factor).  
  5.     h ^= (h >>> 20) ^ (h >>> 12);  
  6.     return h ^ (h >>> 7) ^ (h >>> 4);  
  7. }  
  8.   
  9. /** 
  10.  * Returns index for hash code h. 
  11.  */  
  12. static int indexFor(int h, int length) {  
  13.     return h & (length-1);  
  14. }  
  15.   
  16. int hash = hash(key.hashCode());  
  17. int i = indexFor(hash, table.length);  

HashMap里的put和get方法都使用了这两个方法将key散列到table数组上去。 
indexFor方法是通过hash值和table数组的长度-1进行于操作,来确定具体的位置。 
为什么要减1呢?因为数组的长度是2的n次方,减1以后就变成低位的二进制码都是1,和hash值做与运算的话,就能得到一个小于数组长度的数了。 
那为什么对hashCode还要做一次hash操作呢?因为如果不做hash操作的话,只有低位的值参与了hash的运算,而高位的值没有参加运算。hash方法是让高位的数字也参加hash运算。 
假如:数组的长度是16 我们会发现hashcode为5和53的散列到同一个位置. 
hashcode:53  00000000 00000000 00000000 00110101 
hashcode:5    00000000 00000000 00000000 00000101 
length-1:15     00000000 00000000 00000000 00001111 
只要hashcode值的最后4位是一样的,那么他们就会散列到同一个位置。 
hash方法是通过一些位运算符,让高位的数值也尽可能的参加到运算中,让它尽可能的散列到table数组上,减少hash冲突。 

ConcurrentHashMap的初始化: 
仔细阅读ConcurrentHashMap的构造方法的话,会发现是由initialCapacity,loadFactor, concurrencyLevel几个参数来初始化segments数组的。 
segmentShift和segmentMask是在定位segment时的哈希算法里需要使用的,让其能够尽可能的散列开。 
initialCapacity:ConcurrentHashMap的初始大小 
loadFactor:装载因子 
concurrencyLevel:预想的并发级别,为了能够更好的hash,也保证了concurrencyLevel的值是2的n次方 
segements数组的大小为concurrencyLevel,每个Segement内table的大小为initialCapacity/ concurrencyLevel 

ConcurrentHashMap的put和get 
Java代码  收藏代码
  1. int hash = hash(key.hashCode());  
  2. return segmentFor(hash).get(key, hash);  

可以发现ConcurrentHashMap通过一次hash,两次定位来找到具体的值的。 
先通过segmentFor方法定位到具体的Segment,再在Segment内部定位到具体的HashEntry,而第二次在Segment内部定位的时候是加锁的。 
ConcurrentHashMap的hash算法比HashMap的hash算法更复杂,应该是想让他能够更好的散列到数组上,减少hash冲突。 

HashMap和Segment里modCount的区别: 
modCount都是记录table结构被修改的次数,但是对这个次数的处理上,HashMap和Segment是不一样的。 
HashMap在遍历数据的时候,会判断modCount是否被修改了,如果被修改的话会抛出ConcurrentModificationException异常。 
Segment的modCount在ConcurrentHashMap的containsValue、isEmpty、size方法中用到,ConcurrentHashMap先在不加锁的情况下去做这些计算,如果发现有Segment的modCount被修改了,会再重新获取锁计算。 

HashMap和ConcurrentHashMap的区别: 
如果仔细阅读他们的源码,就会发现HashMap是允许插入key和value是null的数据的,而ConcurrentHashMap是不允许key和value是null的。这个是为什么呢?ConcurrentHashMap的作者是这么说的: 
The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if map.get(key) returns null, you can't detect whether the key explicitly maps to null vs the key isn't mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls. 

为什么重写了equals方法就必须重写hashCode方法呢? 
绝大多数人都知道如果要把一个对象当作key使用的话,就需要重写equals方法。重写了equals方法的话,就必须重写hashCode方法,否则会出现不正确的结果。那么为什么不重写hashCode方法就会出现不正确结果了呢?这个问题只要仔细阅读一下HashMap的put方法,看看它是如何确定一个key是否已存在的就明白了。关键代码: 
Java代码  收藏代码
  1. int hash = hash(key.hashCode());  
  2. int i = indexFor(hash, table.length);  
  3. for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  4.     Object k;  
  5.     if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  6.         V oldValue = e.value;  
  7.         e.value = value;  
  8.         e.recordAccess(this);  
  9.         return oldValue;  
  10.     }  
  11. }  

首先通过key的hashCode来确定具体散列到table的位置,如果这个位置已经有值的话,再通过equals方法判断key是否相等。 
如果只重写equals方法而不重写hashCode方法的话,即使这两个对象通过equals方法判断是相等的,但是因为没有重写hashCode方法,他们的hashCode是不一样的,这样就会被散列到不同的位置去,变成错误的结果了。所以hashCode和equals方法必须一起重写。 

0 0