[Java集合] 彻底搞懂HashMap,HashTable,ConcurrentHashMap之关联.

来源:互联网 发布:淘宝上的喵喵通讯 编辑:程序博客网 时间:2024/05/29 19:17
注: 今天看到的一篇讲hashMap,hashTable,concurrentHashMap很透彻的一篇文章, 感谢原作者的分享.
原文地址:
http://blog.csdn.net/zhangerqing/article/details/8193118

Java集合类是个非常重要的知识点,HashMap、HashTable、ConcurrentHashMap等算是集合类中的重点,可谓“重中之重”,首先来看个问题,如面试官问你:HashMap和HashTable有什么区别,一个比较简单的回答是:

1、HashMap是非线程安全的,HashTable是线程安全的。

2、HashMap的键和值都允许有null值存在,而HashTable则不行。

3、因为线程安全的问题,HashMap效率比HashTable的要高。

能答出上面的三点,简单的面试,算是过了,但是如果再问:Java中的另一个线程安全的与HashMap极其类似的类是什么?同样是线程安全,它与HashTable在线程同步上有什么不同?能把第二个问题完整的答出来,说明你的基础算是不错的了。带着这个问题,本章开始系Java之美[从菜鸟到高手演变]系列之深入解析HashMap和HashTable类应用而生!总想在文章的开头说点儿什么,但又无从说起。从最近的一些面试说起吧,感受就是:知识是永无止境的,永远不要觉得自己已经掌握了某些东西。如果对哪一块知识感兴趣,那么,请多多的花时间,哪怕最基础的东西也要理解它的原理,尽量往深了研究,在学习的同时,记得多与大家交流沟通,因为也许某些东西,从你自己的角度,是很难发现的,因为你并没有那么多的实验环境去发现他们。只有交流的多了,才能及时找出自己的不足,才能认识到:“哦,原来我还有这么多不知道的东西!”。

一、HashMap的内部存储结构
Java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表,数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”,如下图:

从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。它的内部其实是用一个Entity数组来实现的,属性有key、value、next。接下来我会从初始化阶段详细的讲解HashMap的内部结构。

1、初始化
首先来看三个常量:
static final int DEFAULT_INITIAL_CAPACITY = 16; 初始容量:16
static final int MAXIMUM_CAPACITY = 1 
<< 30; 最大容量:2的30次方:1073741824
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
装载因子,后面再说它的作用
来看个无参构造方法,也是我们最常用的:

[java] view plain copy
  1. public HashMap() {  
  2.         this.loadFactor = DEFAULT_LOAD_FACTOR;  
  3.         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);  
  4.         table = new Entry[DEFAULT_INITIAL_CAPACITY];  
  5.         init();  
  6.     }  

loadFactor、threshold的值在此处没有起到作用,不过他们在后面的扩容方面会用到,此处只需理解table=new Entry[DEFAULT_INITIAL_CAPACITY].说明,默认就是开辟16个大小的空间。另外一个重要的构造方法:

[java] view plain copy
  1. public HashMap(int initialCapacity, float loadFactor) {  
  2.         if (initialCapacity < 0)  
  3.             throw new IllegalArgumentException("Illegal initial capacity: " +  
  4.                                                initialCapacity);  
  5.         if (initialCapacity > MAXIMUM_CAPACITY)  
  6.             initialCapacity = MAXIMUM_CAPACITY;  
  7.         if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  8.             throw new IllegalArgumentException("Illegal load factor: " +  
  9.                                                loadFactor);  
  10.   
  11.         // Find a power of 2 >= initialCapacity  
  12.         int capacity = 1;  
  13.         while (capacity < initialCapacity)  
  14.             capacity <<= 1;  
  15.   
  16.         this.loadFactor = loadFactor;  
  17.         threshold = (int)(capacity * loadFactor);  
  18.         table = new Entry[capacity];  
  19.         init();  
  20.     }  


就是说传入参数的构造方法,我们把重点放在:

[java] view plain copy
  1. while (capacity < initialCapacity)  
  2.            capacity <<= 1;  


上面,该代码的意思是,实际的开辟的空间要大于传入的第一个参数的值。举个例子:
new HashMap(7,0.8),loadFactor为0.8,capacity为7,通过上述代码后,capacity的值为:8.(1 << 2的结果是4,2 << 2的结果为8<此处感谢网友wego1234的指正>)。所以,最终capacity的值为8,最后通过new Entry[capacity]来创建大小为capacity的数组,所以,这种方法最红取决于capacity的大小。
2、put(Object key,Object value)操作
 
当调用put操作时,首先判断key是否为null,如下代码1处:

[java] view plain copy
  1. <p>public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }</p><p>        modCount++;  
  15.         addEntry(hash, key, value, i);  
  16.         return null;  
  17.     }</p>  


如果key是null,则调用如下代码:

[java] view plain copy
  1. private V putForNullKey(V value) {  
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
  3.             if (e.key == null) {  
  4.                 V oldValue = e.value;  
  5.                 e.value = value;  
  6.                 e.recordAccess(this);  
  7.                 return oldValue;  
  8.             }  
  9.         }  
  10.         modCount++;  
  11.         addEntry(0null, value, 0);  
  12.         return null;  
  13.     }  


就是说,获取Entry的第一个元素table[0],并基于第一个元素的next属性开始遍历,直到找到key为null的Entry,将其value设置为新的value值。
如果没有找到key为null的元素,则调用如上述代码的addEntry(0, null, value, 0);增加一个新的entry,代码如下:

[java] view plain copy
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     Entry<K,V> e = table[bucketIndex];  
  3.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
  4.         if (size++ >= threshold)  
  5.             resize(2 * table.length);  
  6.     }  


先获取第一个元素table[bucketIndex],传给e对象,新建一个entry,key为null,value为传入的value值,next为获取的e对象。如果容量大于threshold,容量扩大2倍。
如果key不为null,这也是大多数的情况,重新看一下源码:

[java] view plain copy
  1. public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());//---------------2---------------  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {//--------------3-----------  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }//-------------------4------------------  
  15.         modCount++;//----------------5----------  
  16.         addEntry(hash, key, value, i);-------------6-----------  
  17.         return null;  
  18.     }  


看源码中2处,首先会进行key.hashCode()操作,获取key的哈希值,hashCode()是Object类的一个方法,为本地方法,内部实现比较复杂,我们
会在后面作单独的关于Java中Native方法的分析中介绍。hash()的源码如下:

[java] view plain copy
  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.     }  

int i = indexFor(hash, table.length);的意思,相当于int i = hash % Entry[].length;得到i后,就是在Entry数组中的位置,(上述代码5和6处是如果Entry数组中不存在新要增加的元素,则执行5,6处的代码,如果存在,即Hash冲突,则执行 3-4处的代码,此处HashMap中采用链地址法解决Hash冲突。此处经网友bbycszh指正,发现上述陈述有些问题)。重新解释:其实不管Entry数组中i位置有无元素,都会去执行5-6处的代码,如果没有,则直接新增,如果有,则将新元素设置为Entry[0],其next指针指向原有对象,即原有对象为Entry[1]。具体方法可以解释为下面的这段文字:(3-4处的代码只是检查在索引为i的这条链上有没有key重复的,有则替换且返回原值,程序不再去执行5-6处的代码,无则无处理

上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。如, 第一个键值对A进来,通过计算其key的hash得到的i=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其i也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,i也等于0,那么C.next = B,Entry[0] = C;这样我们发现i=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起,也就是说数组中存储的是最后插入的元素。

到这里为止,HashMap的大致实现,我们应该已经清楚了。当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个i的链就会很长,会不会影响性能?HashMap里面设置一个因素(也称为因子),随着map的size越来越大,Entry[]会以一定的规则加长长度。

2、get(Object key)操作
get(Object key)操作时根据键来获取值,如果了解了put操作,get操作容易理解,先来看看源码的实现:

[java] view plain copy
  1. public V get(Object key) {  
  2.         if (key == null)  
  3.             return getForNullKey();  
  4.         int hash = hash(key.hashCode());  
  5.         for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.              e != null;  
  7.              e = e.next) {  
  8.             Object k;  
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))//-------------------1----------------  
  10.                 return e.value;  
  11.         }  
  12.         return null;  
  13.     }  


意思就是:1、当key为null时,调用getForNullKey(),源码如下:

[java] view plain copy
  1. private V getForNullKey() {  
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
  3.             if (e.key == null)  
  4.                 return e.value;  
  5.         }  
  6.         return null;  
  7.     }  

2、当key不为null时,先根据hash函数得到hash值,在更具indexFor()得到i的值,循环遍历链表,如果有:key值等于已存在的key值,则返回其value。如上述get()代码1处判断。

总结下HashMap新增put和获取get操作:

[java] view plain copy
  1. //存储时:  
  2. int hash = key.hashCode();  
  3. int i = hash % Entry[].length;  
  4. Entry[i] = value;  
  5.   
  6. //取值时:  
  7. int hash = key.hashCode();  
  8. int i = hash % Entry[].length;  
  9. return Entry[i];  

理解了就比较简单。

此处附一个简单的HashMap小算法应用:

[java] view plain copy
  1. package com.xtfggef.hashmap;  
  2.   
  3. import java.util.HashMap;  
  4. import java.util.Map;  
  5. import java.util.Set;  
  6.   
  7. /** 
  8.  * 打印在数组中出现n/2以上的元素 
  9.  * 利用一个HashMap来存放数组元素及出现的次数 
  10.  * @author erqing 
  11.  * 
  12.  */  
  13. public class HashMapTest {  
  14.       
  15.     public static void main(String[] args) {  
  16.           
  17.         int [] a = {2,3,2,2,1,4,2,2,2,7,9,6,2,2,3,1,0};  
  18.           
  19.         Map<Integer, Integer> map = new HashMap<Integer,Integer>();  
  20.         for(int i=0; i<a.length; i++){  
  21.             if(map.containsKey(a[i])){  
  22.                 int tmp = map.get(a[i]);  
  23.                 tmp+=1;  
  24.                 map.put(a[i], tmp);  
  25.             }else{  
  26.                 map.put(a[i], 1);  
  27.             }  
  28.         }  
  29.         Set<Integer> set = map.keySet();//------------1------------  
  30.         for (Integer s : set) {  
  31.             if(map.get(s)>=a.length/2){  
  32.                 System.out.println(s);  
  33.             }  
  34.         }//--------------2---------------  
  35.     }  
  36. }  

此处注意两个地方,map.containsKey(),还有就是上述1-2处的代码。

理解了HashMap的上面的操作,其它的大多数方法都很容易理解了。搞清楚它的内部存储机制,一切OK!

二、HashTable的内部存储结构

HashTable和HashMap采用相同的存储机制,二者的实现基本一致,不同的是:

1、HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都是synchronized。

2、HashTable不允许有null值的存在。

在HashTable中调用put方法时,如果key为null,直接抛出NullPointerException。其它细微的差别还有,比如初始化Entry数组的大小等等,但基本思想和HashMap一样。

三、HashTable和ConcurrentHashMap的比较

如我开篇所说一样,ConcurrentHashMap是线程安全的HashMap的实现。同样是线程安全的类,它与HashTable在同步方面有什么不同呢?

之前我们说,synchronized关键字加锁的原理,其实是对对象加锁,不论你是在方法前加synchronized还是语句块前加,锁住的都是对象整体,但是ConcurrentHashMap的同步机制和这个不同,它不是加synchronized关键字,而是基于lock操作的,这样的目的是保证同步的时候,锁住的不是整个对象。事实上,ConcurrentHashMap可以满足concurrentLevel个线程并发无阻塞的操作集合对象。关于concurrentLevel稍后介绍。

1、构造方法

为了容易理解,我们先从构造函数说起。ConcurrentHashMap是基于一个叫Segment数组的,其实和Entry类似,如下:

[java] view plain copy
  1. public ConcurrentHashMap()  
  2.   {  
  3.     this(160.75F, 16);  
  4.   }  


默认传入值16,调用下面的方法:

[java] view plain copy
  1. public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2)  
  2.   {  
  3.     if ((paramFloat <= 0F) || (paramInt1 < 0) || (paramInt2 <= 0))  
  4.       throw new IllegalArgumentException();  
  5.   
  6.     if (paramInt2 > 65536) {  
  7.       paramInt2 = 65536;  
  8.     }  
  9.   
  10.     int i = 0;  
  11.     int j = 1;  
  12.     while (j < paramInt2) {  
  13.       ++i;  
  14.       j <<= 1;  
  15.     }  
  16.     this.segmentShift = (32 - i);  
  17.     this.segmentMask = (j - 1);  
  18.     this.segments = Segment.newArray(j);  
  19.   
  20.     if (paramInt1 > 1073741824)  
  21.       paramInt1 = 1073741824;  
  22.     int k = paramInt1 / j;  
  23.     if (k * j < paramInt1)  
  24.       ++k;  
  25.     int l = 1;  
  26.     while (l < k)  
  27.       l <<= 1;  
  28.   
  29.     for (int i1 = 0; i1 < this.segments.length; ++i1)  
  30.       this.segments[i1] = new Segment(l, paramFloat);  
  31.   }  


你会发现比HashMap的构造函数多一个参数,paramInt1就是我们之前谈过的initialCapacity,就是数组的初始化大小,paramfloat为loadFactor(装载因子),而paramInt2则是我们所要说的concurrentLevel,这三个值分别被初始化为16,0.75,16,经过:

[java] view plain copy
  1. while (j < paramInt2) {  
  2.       ++i;  
  3.       j <<= 1;  
  4.     }  


后,j就是我们最终要开辟的数组的size值,当paramInt1为16时,计算出来的size值就是16.通过:

this.segments = Segment.newArray(j)后,我们看出了,最终稿创建的Segment数组的大小为16.最终创建Segment对象时:

[java] view plain copy
  1. this.segments[i1] = new Segment(cap, paramFloat);  

需要cap值,而cap值来源于:

[java] view plain copy
  1. int k = paramInt1 / j;  
  2.   if (k * j < paramInt1)  
  3.     ++k;  
  4.   int cap = 1;  
  5.   while (cap < k)  
  6.     cap <<= 1;  

组后创建大小为cap的数组。最后根据数组的大小及paramFloat的值算出了threshold的值:

this.threshold = (int)(paramArrayOfHashEntry.length * this.loadFactor)。

2、put操作

[java] view plain copy
  1. public V put(K paramK, V paramV)  
  2.   {  
  3.     if (paramV == null)  
  4.       throw new NullPointerException();  
  5.     int i = hash(paramK.hashCode());  
  6.     return segmentFor(i).put(paramK, i, paramV, false);  
  7.   }  


与HashMap不同的是,如果key为null,直接抛出NullPointer异常,之后,同样先计算hashCode的值,再计算hash值,不过此处hash函数和HashMap中的不一样:

[java] view plain copy
  1. private static int hash(int paramInt)  
  2.   {  
  3.     paramInt += (paramInt << 15 ^ 0xFFFFCD7D);  
  4.     paramInt ^= paramInt >>> 10;  
  5.     paramInt += (paramInt << 3);  
  6.     paramInt ^= paramInt >>> 6;  
  7.     paramInt += (paramInt << 2) + (paramInt << 14);  
  8.     return (paramInt ^ paramInt >>> 16);  
  9.   }  


 

[java] view plain copy
  1. final Segment<K, V> segmentFor(int paramInt)  
  2.   {  
  3.     return this.segments[(paramInt >>> this.segmentShift & this.segmentMask)];  
  4.   }  


根据上述代码找到Segment对象后,调用put来操作:

[java] view plain copy
  1. V put(K paramK, int paramInt, V paramV, boolean paramBoolean)  
  2. {  
  3.   lock();  
  4.   try {  
  5.     Object localObject1;  
  6.     Object localObject2;  
  7.     int i = this.count;  
  8.     if (i++ > this.threshold)  
  9.       rehash();  
  10.     ConcurrentHashMap.HashEntry[] arrayOfHashEntry = this.table;  
  11.     int j = paramInt & arrayOfHashEntry.length - 1;  
  12.     ConcurrentHashMap.HashEntry localHashEntry1 = arrayOfHashEntry[j];  
  13.     ConcurrentHashMap.HashEntry localHashEntry2 = localHashEntry1;  
  14.     while ((localHashEntry2 != null) && (((localHashEntry2.hash != paramInt) || (!(paramK.equals(localHashEntry2.key)))))) {  
  15.       localHashEntry2 = localHashEntry2.next;  
  16.     }  
  17.   
  18.     if (localHashEntry2 != null) {  
  19.       localObject1 = localHashEntry2.value;  
  20.       if (!(paramBoolean))  
  21.         localHashEntry2.value = paramV;  
  22.     }  
  23.     else {  
  24.       localObject1 = null;  
  25.       this.modCount += 1;  
  26.       arrayOfHashEntry[j] = new ConcurrentHashMap.HashEntry(paramK, paramInt, localHashEntry1, paramV);  
  27.       this.count = i;  
  28.     }  
  29.     return localObject1;  
  30.   } finally {  
  31.     unlock();  
  32.   }  
  33. }  


先调用lock(),lock是ReentrantLock类的一个方法,用当前存储的个数+1来和threshold比较,如果大于threshold,则进行rehash,将当前的容量扩大2倍,重新进行hash。之后对hash的值和数组大小-1进行按位于操作后,得到当前的key需要放入的位置,从这儿开始,和HashMap一样。

从上述的分析看出,ConcurrentHashMap基于concurrentLevel划分出了多个Segment来对key-value进行存储,从而避免每次锁定整个数组,在默认的情况下,允许16个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。

在多线程的环境中,相对于HashTable,ConcurrentHashMap会带来很大的性能提升!

欢迎读者批评指正,有任何建议请联系:

EGG:xtfggef@gmail.com      http://weibo.com/xtfggef

四、HashMap常见问题分析

1、此处我觉得网友huxb23@126的一篇文章说的很好,分析多线程并发写HashMap线程被hang住的原因 ,因为是优秀的资源,此处我整理下搬到这儿。

以下内容转自博文:http://blog.163.com/huxb23@126/blog/static/625898182011211318854/ 

先看原问题代码:

[java] view plain copy
  1. import java.util.HashMap;  
  2.   
  3. public class TestLock {  
  4.   
  5.     private HashMap map = new HashMap();  
  6.   
  7.     public TestLock() {  
  8.         Thread t1 = new Thread() {  
  9.             public void run() {  
  10.                 for (int i = 0; i < 50000; i++) {  
  11.                     map.put(new Integer(i), i);  
  12.                 }  
  13.                 System.out.println("t1 over");  
  14.             }  
  15.         };  
  16.   
  17.         Thread t2 = new Thread() {  
  18.             public void run() {  
  19.                 for (int i = 0; i < 50000; i++) {  
  20.                     map.put(new Integer(i), i);  
  21.                 }  
  22.   
  23.                 System.out.println("t2 over");  
  24.             }  
  25.         };  
  26.   
  27.         t1.start();  
  28.         t2.start();  
  29.   
  30.     }  
  31.   
  32.     public static void main(String[] args) {  
  33.         new TestLock();  
  34.     }  
  35. }  


就是启了两个线程,不断的往一个非线程安全的HashMap中put内容,put的内容很简单,key和value都是从0自增的整数(这个put的内容做的并不好,以致于后来干扰了我分析问题的思路)。对HashMap做并发写操作,我原以为只不过会产生脏数据的情况,但反复运行这个程序,会出现线程t1、t2被hang住的情况,多数情况下是一个线程被hang住另一个成功结束,偶尔会两个线程都被hang住。说到这里,你如果觉得不好好学习ConcurrentHashMap而在这瞎折腾就手下留情跳过吧。
好吧,分析下HashMap的put函数源码看看问题出在哪,这里就罗列出相关代码(jdk1.6):

[java] view plain copy
  1. public V put(K paramK, V paramV)  
  2. {  
  3.   if (paramK == null)  
  4.     return putForNullKey(paramV);  
  5.   int i = hash(paramK.hashCode());  
  6.   int j = indexFor(i, this.table.length);  
  7.   for (Entry localEntry = this.table[j]; localEntry != null; localEntry = localEntry.next)  
  8.   {  
  9.     if (localEntry.hash == i) { java.lang.Object localObject1;  
  10.       if (((localObject1 = localEntry.key) == paramK) || (paramK.equals(localObject1))) {  
  11.         java.lang.Object localObject2 = localEntry.value;  
  12.         localEntry.value = paramV;  
  13.         localEntry.recordAccess(this);  
  14.         return localObject2;  
  15.       }  
  16.     }  
  17.   }  
  18.   this.modCount += 1;  
  19.   addEntry(i, paramK, paramV, j);  
  20.   return null;  
  21. }  
  22.   
  23. private V putForNullKey(V paramV)  
  24. {  
  25.   for (Entry localEntry = this.table[0]; localEntry != null; localEntry = localEntry.next)  
  26.     if (localEntry.key == null) {  
  27.       java.lang.Object localObject = localEntry.value;  
  28.       localEntry.value = paramV;  
  29.       localEntry.recordAccess(this);  
  30.       return localObject;  
  31.     }  
  32.   
  33.   this.modCount += 1;  
  34.   addEntry(0null, paramV, 0);  
  35.   return null;  
  36. }  

 

通过jconsole(或者thread dump),可以看到线程停在了transfer方法的while循环处。这个transfer方法的作用是,当Map中元素数超过阈值需要resize时,它负责把原Map中的元素映射到新Map中。我修改了HashMap,加上了@标记2和@标记3的代码片断,以打印出死循环时的状态,结果死循环线程总是出现类似这样的输出:“Thread-1,e==next:false,e==next.next:true,e:108928=108928,next:108928=108928,eq:true”。
这个输出表明:
1)这个Entry链中的两个Entry之间的关系是:e=e.next.next,造成死循环。
2)e.equals(e.next),但e!=e.next。因为测试例子中两个线程put的内容一样,并发时可能同一个key被保存了多个value,这种错误是在addEntry函数产生的,但这和线程死循环没有关系。

接下来就分析transfer中那个while循环了。先所说这个循环正常的功能:src[j]保存的是映射成同一个hash值的多个Entry的链表,这个src[j]可能为null,可能只有一个Entry,也可能由多个Entry链接起来。假设是多个Entry,原来的链是(src[j]=a)->b(也就是src[j]=a,a.next=b,b.next=null),经过while处理后得到了(newTable[i]=b)->a。也就是说,把链表的next关系反向了。

再看看这个while中可能在多线程情况下引起问题的语句。针对两个线程t1和t2,这里它们可能的产生问题的执行序列做些个人分析:

1)假设同一个Entry列表[e->f->...],t1先到,t2后到并都走到while中。t1执行“e.next = newTable[i];newTable[i] = e;”这使得e.next=null(初始的newTable[i]为null),newTable[i]指向了e。这时t2执行了“e.next = newTable[i];newTable[i] = e;”,这使得e.next=e,e死循环了。因为循环开始处的“final Entry next = e.next;”,尽管e自己死循环了,在最后的“e = next;”后,两个线程都会跳过e继续执行下去。

2)在while中逐个遍历Entry链表中的Entry而把next关系反向时,newTable[i]成为了被交换的引用,可疑的语句在于“e.next = newTable[i];”。假设链表e->f->g被t1处理成e<-f<-g,newTable[i]指向了g,这时t2进来了,它一执行“e.next = newTable[i];”就使得e->g,造成了死循环。所以,理论上来说,死循环的Entry个数可能很多。尽管产生了死循环,但是t1执行到了死循环的右边,所以是会继续执行下去的,而t2如果执行“final Entry next = e.next;”的next为null,则也会继续执行下去,否则就进入了死循环。

3)似乎情况会更复杂,因为即便线程跳出了死循环,它下一次做resize进入transfer时,有可能因为之前的死循环Entry链表而被hang住(似乎是一定会被hang住)。也有可能,在put检查Entry链表时(@标记1),因为Entry链表的死循环而被hang住。也似乎有可能,活着的线程和死循环的线程同时执行在while里后,两个线程都能活着出去。所以,可能两个线程平安退出,可能一个线程hang在transfer中,可能两个线程都被hang住而又不一定在一个地方。

4)我反复的测试,出现一个线程被hang住的情况最多,都是e=e.next.next造成的,这主要就是例子put两份增量数据造成的。我如果去掉@标记3的输出,有时也能复现两个线程都被hang住的情况,但加上后就很难复现出来。我又把put的数据改了下,比如让两个线程put范围不同的数据,就能复现出e=e.next,两个线程都被hang住的情况。

上面罗哩罗嗦了很多,一开始我简单的分析后觉得似乎明白了怎么回事,可现在仔细琢磨后似乎又不明白了许多。有一个细节是,每次死循环的key的大小也是有据可循的,我就不打哈了。感觉,如果样本多些,可能出现问题的原因点会很多,也会更复杂,我姑且不再蛋疼下去。至于有人提到ConcurrentHashMap也有这个问题,我觉得不大可能,因为它的put操作是加锁的,如果有这个问题就不叫线程安全的Map了。

2、HashMap中Value可以相同,但是键不可以相同

当插入HashMap的key相同时,会覆盖原有的Value,且返回原Value值,看下面的程序:

[java] view plain copy
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) {  
  4.           
  5.         HashMap<String,Integer> map = new HashMap<String,Integer>();  
  6.   
  7.         //出入两个Value相同的值,没有问题  
  8.         map.put("egg"1);  
  9.         map.put("niu"1);  
  10.           
  11.         //插入key相同的值,看返回结果  
  12.         int egg = (Integer) map.put("egg"3);  
  13.           
  14.         System.out.println(egg);   //输出1  
  15.         System.out.println(map.get("egg"));   //输出3,将原值1覆盖  
  16.         System.out.println(map.get("niu"));   //输出1  
  17.     }  
  18. }  

相同的键会被覆盖,且返回原值。

3、HashMap按值排序

给定一个数组,求出每个数据出现的次数并按照次数的由大到小排列出来。我们选用HashMap来做,key存储数组元素,值存储出现的次数,最后用Collections的sort方法对HashMap的值进行排序。代码如下:

[java] view plain copy
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) {  
  4.   
  5.         int data[] = { 252352352352352,  
  6.                 78878790 };  
  7.         Map<Integer, Integer> map = new HashMap<Integer, Integer>();  
  8.         for (int i : data) {  
  9.             if (map.containsKey(i)) {//判断HashMap里是否存在  
  10.                 map.put(i, map.get(i) + 1);//已存在,值+1  
  11.             } else {  
  12.                 map.put(i, 1);//不存在,新增  
  13.             }  
  14.         }  
  15.         //map按值排序  
  16.         List<Map.Entry<Integer, Integer>> list = new ArrayList<Map.Entry<Integer, Integer>>(  
  17.                 map.entrySet());  
  18.         Collections.sort(list, new Comparator<Map.Entry<Integer, Integer>>() {  
  19.             public int compare(Map.Entry<Integer, Integer> o1,  
  20.                     Map.Entry<Integer, Integer> o2) {  
  21.                 return (o2.getValue() - o1.getValue());  
  22.             }  
  23.         });  
  24.         for (Map.Entry<Integer, Integer> m : list) {  
  25.             System.out.println(m.getKey() + "-" + m.getValue());  
  26.         }  
  27.     }  
  28.   
  29. }  

输出:

2-6
5-5
3-4
8-3
7-3
9-1
0-1

  1. public HashMap() { 
  2.         this.loadFactor = DEFAULT_LOAD_FACTOR; 
  3.         threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); 
  4.         table = new Entry[DEFAULT_INITIAL_CAPACITY]; 
  5.         init(); 
  6.     } 

loadFactor、threshold的值在此处没有起到作用,不过他们在后面的扩容方面会用到,此处只需理解table=new Entry[DEFAULT_INITIAL_CAPACITY].说明,默认就是开辟16个大小的空间。另外一个重要的构造方法:

[java] view plain copy
  1. public HashMap(int initialCapacity, float loadFactor) { 
  2.         if (initialCapacity < 0
  3.             throw new IllegalArgumentException("Illegal initial capacity: "
  4.                                                initialCapacity); 
  5.         if (initialCapacity > MAXIMUM_CAPACITY) 
  6.             initialCapacity = MAXIMUM_CAPACITY; 
  7.         if (loadFactor <= 0 || Float.isNaN(loadFactor)) 
  8.             throw new IllegalArgumentException("Illegal load factor: "
  9.                                                loadFactor); 
  10.  
  11.         // Find a power of 2 >= initialCapacity 
  12.         int capacity = 1
  13.         while (capacity < initialCapacity) 
  14.             capacity <<= 1
  15.  
  16.         this.loadFactor = loadFactor; 
  17.         threshold = (int)(capacity * loadFactor); 
  18.         table = new Entry[capacity]; 
  19.         init(); 
  20.     } 


就是说传入参数的构造方法,我们把重点放在:

[java] view plain copy
  1. while (capacity < initialCapacity) 
  2.            capacity <<= 1


上面,该代码的意思是,实际的开辟的空间要大于传入的第一个参数的值。举个例子:
new HashMap(7,0.8),loadFactor为0.8,capacity为7,通过上述代码后,capacity的值为:8.(1 << 2的结果是4,2 << 2的结果为8<此处感谢网友wego1234的指正>)。所以,最终capacity的值为8,最后通过new Entry[capacity]来创建大小为capacity的数组,所以,这种方法最红取决于capacity的大小。
2、put(Object key,Object value)操作
 
当调用put操作时,首先判断key是否为null,如下代码1处:

[java] view plain copy
  1. <p>public V put(K key, V value) { 
  2.         if (key == null
  3.             return putForNullKey(value); 
  4.         int hash = hash(key.hashCode()); 
  5.         int i = indexFor(hash, table.length); 
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
  7.             Object k; 
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
  9.                 V oldValue = e.value; 
  10.                 e.value = value; 
  11.                 e.recordAccess(this); 
  12.                 return oldValue; 
  13.             } 
  14.         }</p><p>        modCount++; 
  15.         addEntry(hash, key, value, i); 
  16.         return null
  17.     }</p> 


如果key是null,则调用如下代码:

[java] view plain copy
  1. private V putForNullKey(V value) { 
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) { 
  3.             if (e.key == null) { 
  4.                 V oldValue = e.value; 
  5.                 e.value = value; 
  6.                 e.recordAccess(this); 
  7.                 return oldValue; 
  8.             } 
  9.         } 
  10.         modCount++; 
  11.         addEntry(0, null, value, 0); 
  12.         return null
  13.     } 


就是说,获取Entry的第一个元素table[0],并基于第一个元素的next属性开始遍历,直到找到key为null的Entry,将其value设置为新的value值。
如果没有找到key为null的元素,则调用如上述代码的addEntry(0, null, value, 0);增加一个新的entry,代码如下:

[java] view plain copy
  1. void addEntry(int hash, K key, V value, int bucketIndex) { 
  2.     Entry<K,V> e = table[bucketIndex]; 
  3.         table[bucketIndex] = new Entry<K,V>(hash, key, value, e); 
  4.         if (size++ >= threshold) 
  5.             resize(2 * table.length); 
  6.     } 


先获取第一个元素table[bucketIndex],传给e对象,新建一个entry,key为null,value为传入的value值,next为获取的e对象。如果容量大于threshold,容量扩大2倍。
如果key不为null,这也是大多数的情况,重新看一下源码:

[java] view plain copy
  1. public V put(K key, V value) { 
  2.         if (key == null
  3.             return putForNullKey(value); 
  4.         int hash = hash(key.hashCode());//---------------2--------------- 
  5.         int i = indexFor(hash, table.length); 
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {//--------------3----------- 
  7.             Object k; 
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
  9.                 V oldValue = e.value; 
  10.                 e.value = value; 
  11.                 e.recordAccess(this); 
  12.                 return oldValue; 
  13.             } 
  14.         }//-------------------4------------------ 
  15.         modCount++;//----------------5---------- 
  16.         addEntry(hash, key, value, i);-------------6----------- 
  17.         return null
  18.     } 


看源码中2处,首先会进行key.hashCode()操作,获取key的哈希值,hashCode()是Object类的一个方法,为本地方法,内部实现比较复杂,我们
会在后面作单独的关于Java中Native方法的分析中介绍。hash()的源码如下:

[java] view plain copy
  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.     } 

int i = indexFor(hash, table.length);的意思,相当于int i = hash % Entry[].length;得到i后,就是在Entry数组中的位置,(上述代码5和6处是如果Entry数组中不存在新要增加的元素,则执行5,6处的代码,如果存在,即Hash冲突,则执行 3-4处的代码,此处HashMap中采用链地址法解决Hash冲突。此处经网友bbycszh指正,发现上述陈述有些问题)。重新解释:其实不管Entry数组中i位置有无元素,都会去执行5-6处的代码,如果没有,则直接新增,如果有,则将新元素设置为Entry[0],其next指针指向原有对象,即原有对象为Entry[1]。具体方法可以解释为下面的这段文字:(3-4处的代码只是检查在索引为i的这条链上有没有key重复的,有则替换且返回原值,程序不再去执行5-6处的代码,无则无处理

上面我们提到过Entry类里面有一个next属性,作用是指向下一个Entry。如, 第一个键值对A进来,通过计算其key的hash得到的i=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其i也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,i也等于0,那么C.next = B,Entry[0] = C;这样我们发现i=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起,也就是说数组中存储的是最后插入的元素。

到这里为止,HashMap的大致实现,我们应该已经清楚了。当然HashMap里面也包含一些优化方面的实现,这里也说一下。比如:Entry[]的长度一定后,随着map里面数据的越来越长,这样同一个i的链就会很长,会不会影响性能?HashMap里面设置一个因素(也称为因子),随着map的size越来越大,Entry[]会以一定的规则加长长度。

2、get(Object key)操作
get(Object key)操作时根据键来获取值,如果了解了put操作,get操作容易理解,先来看看源码的实现:

[java] view plain copy
  1. public V get(Object key) { 
  2.         if (key == null
  3.             return getForNullKey(); 
  4.         int hash = hash(key.hashCode()); 
  5.         for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
  6.              e != null
  7.              e = e.next) { 
  8.             Object k; 
  9.             if (e.hash == hash && ((k = e.key) == key || key.equals(k)))//-------------------1---------------- 
  10.                 return e.value; 
  11.         } 
  12.         return null
  13.     } 


意思就是:1、当key为null时,调用getForNullKey(),源码如下:

[java] view plain copy
  1. private V getForNullKey() { 
  2.         for (Entry<K,V> e = table[0]; e != null; e = e.next) { 
  3.             if (e.key == null
  4.                 return e.value; 
  5.         } 
  6.         return null
  7.     } 

2、当key不为null时,先根据hash函数得到hash值,在更具indexFor()得到i的值,循环遍历链表,如果有:key值等于已存在的key值,则返回其value。如上述get()代码1处判断。

总结下HashMap新增put和获取get操作:

[java] view plain copy
  1. //存储时: 
  2. int hash = key.hashCode(); 
  3. int i = hash % Entry[].length; 
  4. Entry[i] = value; 
  5.  
  6. //取值时: 
  7. int hash = key.hashCode(); 
  8. int i = hash % Entry[].length; 
  9. return Entry[i]; 

理解了就比较简单。

此处附一个简单的HashMap小算法应用:

[java] view plain copy
  1. package com.xtfggef.hashmap; 
  2.  
  3. import java.util.HashMap; 
  4. import java.util.Map; 
  5. import java.util.Set; 
  6.  
  7. /** 
  8. * 打印在数组中出现n/2以上的元素 
  9. * 利用一个HashMap来存放数组元素及出现的次数 
  10. * @author erqing 
  11. * 
  12. */ 
  13. public class HashMapTest { 
  14.      
  15.     public static void main(String[] args) { 
  16.          
  17.         int [] a = {2,3,2,2,1,4,2,2,2,7,9,6,2,2,3,1,0}; 
  18.          
  19.         Map<Integer, Integer> map = new HashMap<Integer,Integer>(); 
  20.         for(int i=0; i<a.length; i++){ 
  21.             if(map.containsKey(a[i])){ 
  22.                 int tmp = map.get(a[i]); 
  23.                 tmp+=1
  24.                 map.put(a[i], tmp); 
  25.             }else
  26.                 map.put(a[i], 1); 
  27.             } 
  28.         } 
  29.         Set<Integer> set = map.keySet();//------------1------------ 
  30.         for (Integer s : set) { 
  31.             if(map.get(s)>=a.length/2){ 
  32.                 System.out.println(s); 
  33.             } 
  34.         }//--------------2--------------- 
  35.     } 

此处注意两个地方,map.containsKey(),还有就是上述1-2处的代码。

理解了HashMap的上面的操作,其它的大多数方法都很容易理解了。搞清楚它的内部存储机制,一切OK!

二、HashTable的内部存储结构

HashTable和HashMap采用相同的存储机制,二者的实现基本一致,不同的是:

1、HashMap是非线程安全的,HashTable是线程安全的,内部的方法基本都是synchronized。

2、HashTable不允许有null值的存在。

在HashTable中调用put方法时,如果key为null,直接抛出NullPointerException。其它细微的差别还有,比如初始化Entry数组的大小等等,但基本思想和HashMap一样。

三、HashTable和ConcurrentHashMap的比较

如我开篇所说一样,ConcurrentHashMap是线程安全的HashMap的实现。同样是线程安全的类,它与HashTable在同步方面有什么不同呢?

之前我们说,synchronized关键字加锁的原理,其实是对对象加锁,不论你是在方法前加synchronized还是语句块前加,锁住的都是对象整体,但是ConcurrentHashMap的同步机制和这个不同,它不是加synchronized关键字,而是基于lock操作的,这样的目的是保证同步的时候,锁住的不是整个对象。事实上,ConcurrentHashMap可以满足concurrentLevel个线程并发无阻塞的操作集合对象。关于concurrentLevel稍后介绍。

1、构造方法

为了容易理解,我们先从构造函数说起。ConcurrentHashMap是基于一个叫Segment数组的,其实和Entry类似,如下:

[java] view plain copy
  1. public ConcurrentHashMap() 
  2.   { 
  3.     this(16, 0.75F, 16); 
  4.   } 


默认传入值16,调用下面的方法:

[java] view plain copy
  1. public ConcurrentHashMap(int paramInt1, float paramFloat, int paramInt2) 
  2.   { 
  3.     if ((paramFloat <= 0F) || (paramInt1 < 0) || (paramInt2 <= 0)) 
  4.       throw new IllegalArgumentException(); 
  5.  
  6.     if (paramInt2 > 65536) { 
  7.       paramInt2 = 65536
  8.     } 
  9.  
  10.     int i = 0
  11.     int j = 1
  12.     while (j < paramInt2) { 
  13.       ++i; 
  14.       j <<= 1
  15.     } 
  16.     this.segmentShift = (32 - i); 
  17.     this.segmentMask = (j - 1); 
  18.     this.segments = Segment.newArray(j); 
  19.  
  20.     if (paramInt1 > 1073741824
  21.       paramInt1 = 1073741824
  22.     int k = paramInt1 / j; 
  23.     if (k * j < paramInt1) 
  24.       ++k; 
  25.     int l = 1
  26.     while (l < k) 
  27.       l <<= 1
  28.  
  29.     for (int i1 = 0; i1 < this.segments.length; ++i1) 
  30.       this.segments[i1] = new Segment(l, paramFloat); 
  31.   } 


你会发现比HashMap的构造函数多一个参数,paramInt1就是我们之前谈过的initialCapacity,就是数组的初始化大小,paramfloat为loadFactor(装载因子),而paramInt2则是我们所要说的concurrentLevel,这三个值分别被初始化为16,0.75,16,经过:

[java] view plain copy
  1. while (j < paramInt2) { 
  2.       ++i; 
  3.       j <<= 1
  4.     } 


后,j就是我们最终要开辟的数组的size值,当paramInt1为16时,计算出来的size值就是16.通过:

this.segments = Segment.newArray(j)后,我们看出了,最终稿创建的Segment数组的大小为16.最终创建Segment对象时:

[java] view plain copy
  1. this.segments[i1] = new Segment(cap, paramFloat); 

需要cap值,而cap值来源于:

[java] view plain copy
  1. int k = paramInt1 / j; 
  2.   if (k * j < paramInt1) 
  3.     ++k; 
  4.   int cap = 1
  5.   while (cap < k) 
  6.     cap <<= 1

组后创建大小为cap的数组。最后根据数组的大小及paramFloat的值算出了threshold的值:

this.threshold = (int)(paramArrayOfHashEntry.length * this.loadFactor)。

2、put操作

[java] view plain copy
  1. public V put(K paramK, V paramV) 
  2.   { 
  3.     if (paramV == null
  4.       throw new NullPointerException(); 
  5.     int i = hash(paramK.hashCode()); 
  6.     return segmentFor(i).put(paramK, i, paramV, false); 
  7.   } 


与HashMap不同的是,如果key为null,直接抛出NullPointer异常,之后,同样先计算hashCode的值,再计算hash值,不过此处hash函数和HashMap中的不一样:

[java] view plain copy
  1. private static int hash(int paramInt) 
  2.   { 
  3.     paramInt += (paramInt << 15 ^ 0xFFFFCD7D); 
  4.     paramInt ^= paramInt >>> 10
  5.     paramInt += (paramInt << 3); 
  6.     paramInt ^= paramInt >>> 6
  7.     paramInt += (paramInt << 2) + (paramInt << 14); 
  8.     return (paramInt ^ paramInt >>> 16); 
  9.   } 


 

[java] view plain copy
  1. final Segment<K, V> segmentFor(int paramInt) 
  2.   { 
  3.     return this.segments[(paramInt >>> this.segmentShift & this.segmentMask)]; 
  4.   } 


根据上述代码找到Segment对象后,调用put来操作:

[java] view plain copy
  1. V put(K paramK, int paramInt, V paramV, boolean paramBoolean) 
  2.   lock(); 
  3.   try
  4.     Object localObject1; 
  5.     Object localObject2; 
  6.     int i = this.count; 
  7.     if (i++ > this.threshold) 
  8.       rehash(); 
  9.     ConcurrentHashMap.HashEntry[] arrayOfHashEntry = this.table; 
  10.     int j = paramInt & arrayOfHashEntry.length - 1
  11.     ConcurrentHashMap.HashEntry localHashEntry1 = arrayOfHashEntry[j]; 
  12.     ConcurrentHashMap.HashEntry localHashEntry2 = localHashEntry1; 
  13.     while ((localHashEntry2 != null) && (((localHashEntry2.hash != paramInt) || (!(paramK.equals(localHashEntry2.key)))))) { 
  14.       localHashEntry2 = localHashEntry2.next; 
  15.     } 
  16.  
  17.     if (localHashEntry2 != null) { 
  18.       localObject1 = localHashEntry2.value; 
  19.       if (!(paramBoolean)) 
  20.         localHashEntry2.value = paramV; 
  21.     } 
  22.     else
  23.       localObject1 = null
  24.       this.modCount += 1
  25.       arrayOfHashEntry[j] = new ConcurrentHashMap.HashEntry(paramK, paramInt, localHashEntry1, paramV); 
  26.       this.count = i; 
  27.     } 
  28.     return localObject1; 
  29.   } finally
  30.     unlock(); 
  31.   } 


先调用lock(),lock是ReentrantLock类的一个方法,用当前存储的个数+1来和threshold比较,如果大于threshold,则进行rehash,将当前的容量扩大2倍,重新进行hash。之后对hash的值和数组大小-1进行按位于操作后,得到当前的key需要放入的位置,从这儿开始,和HashMap一样。

从上述的分析看出,ConcurrentHashMap基于concurrentLevel划分出了多个Segment来对key-value进行存储,从而避免每次锁定整个数组,在默认的情况下,允许16个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。

在多线程的环境中,相对于HashTable,ConcurrentHashMap会带来很大的性能提升!

四、HashMap常见问题分析

1、此处我觉得网友huxb23@126的一篇文章说的很好,分析多线程并发写HashMap线程被hang住的原因 ,因为是优秀的资源,此处我整理下搬到这儿。

以下内容转自博文:http://blog.163.com/huxb23@126/blog/static/625898182011211318854/ 

先看原问题代码:

[java] view plain copy
  1. import java.util.HashMap; 
  2.  
  3. public class TestLock { 
  4.  
  5.     private HashMap map = new HashMap(); 
  6.  
  7.     public TestLock() { 
  8.         Thread t1 = new Thread() { 
  9.             public void run() { 
  10.                 for (int i = 0; i < 50000; i++) { 
  11.                     map.put(new Integer(i), i); 
  12.                 } 
  13.                 System.out.println("t1 over"); 
  14.             } 
  15.         }; 
  16.  
  17.         Thread t2 = new Thread() { 
  18.             public void run() { 
  19.                 for (int i = 0; i < 50000; i++) { 
  20.                     map.put(new Integer(i), i); 
  21.                 } 
  22.  
  23.                 System.out.println("t2 over"); 
  24.             } 
  25.         }; 
  26.  
  27.         t1.start(); 
  28.         t2.start(); 
  29.  
  30.     } 
  31.  
  32.     public static void main(String[] args) { 
  33.         new TestLock(); 
  34.     } 


就是启了两个线程,不断的往一个非线程安全的HashMap中put内容,put的内容很简单,key和value都是从0自增的整数(这个put的内容做的并不好,以致于后来干扰了我分析问题的思路)。对HashMap做并发写操作,我原以为只不过会产生脏数据的情况,但反复运行这个程序,会出现线程t1、t2被hang住的情况,多数情况下是一个线程被hang住另一个成功结束,偶尔会两个线程都被hang住。说到这里,你如果觉得不好好学习ConcurrentHashMap而在这瞎折腾就手下留情跳过吧。
好吧,分析下HashMap的put函数源码看看问题出在哪,这里就罗列出相关代码(jdk1.6):

[java] view plain copy
  1. public V put(K paramK, V paramV) 
  2.   if (paramK == null
  3.     return putForNullKey(paramV); 
  4.   int i = hash(paramK.hashCode()); 
  5.   int j = indexFor(i, this.table.length); 
  6.   for (Entry localEntry = this.table[j]; localEntry != null; localEntry = localEntry.next) 
  7.   { 
  8.     if (localEntry.hash == i) { java.lang.Object localObject1; 
  9.       if (((localObject1 = localEntry.key) == paramK) || (paramK.equals(localObject1))) { 
  10.         java.lang.Object localObject2 = localEntry.value; 
  11.         localEntry.value = paramV; 
  12.         localEntry.recordAccess(this); 
  13.         return localObject2; 
  14.       } 
  15.     } 
  16.   } 
  17.   this.modCount += 1
  18.   addEntry(i, paramK, paramV, j); 
  19.   return null
  20.  
  21. private V putForNullKey(V paramV) 
  22.   for (Entry localEntry = this.table[0]; localEntry != null; localEntry = localEntry.next) 
  23.     if (localEntry.key == null) { 
  24.       java.lang.Object localObject = localEntry.value; 
  25.       localEntry.value = paramV; 
  26.       localEntry.recordAccess(this); 
  27.       return localObject; 
  28.     } 
  29.  
  30.   this.modCount += 1
  31.   addEntry(0, null, paramV, 0); 
  32.   return null

 

通过jconsole(或者thread dump),可以看到线程停在了transfer方法的while循环处。这个transfer方法的作用是,当Map中元素数超过阈值需要resize时,它负责把原Map中的元素映射到新Map中。我修改了HashMap,加上了@标记2和@标记3的代码片断,以打印出死循环时的状态,结果死循环线程总是出现类似这样的输出:“Thread-1,e==next:false,e==next.next:true,e:108928=108928,next:108928=108928,eq:true”。
这个输出表明:
1)这个Entry链中的两个Entry之间的关系是:e=e.next.next,造成死循环。
2)e.equals(e.next),但e!=e.next。因为测试例子中两个线程put的内容一样,并发时可能同一个key被保存了多个value,这种错误是在addEntry函数产生的,但这和线程死循环没有关系。

接下来就分析transfer中那个while循环了。先所说这个循环正常的功能:src[j]保存的是映射成同一个hash值的多个Entry的链表,这个src[j]可能为null,可能只有一个Entry,也可能由多个Entry链接起来。假设是多个Entry,原来的链是(src[j]=a)->b(也就是src[j]=a,a.next=b,b.next=null),经过while处理后得到了(newTable[i]=b)->a。也就是说,把链表的next关系反向了。

再看看这个while中可能在多线程情况下引起问题的语句。针对两个线程t1和t2,这里它们可能的产生问题的执行序列做些个人分析:

1)假设同一个Entry列表[e->f->...],t1先到,t2后到并都走到while中。t1执行“e.next = newTable[i];newTable[i] = e;”这使得e.next=null(初始的newTable[i]为null),newTable[i]指向了e。这时t2执行了“e.next = newTable[i];newTable[i] = e;”,这使得e.next=e,e死循环了。因为循环开始处的“final Entry next = e.next;”,尽管e自己死循环了,在最后的“e = next;”后,两个线程都会跳过e继续执行下去。

2)在while中逐个遍历Entry链表中的Entry而把next关系反向时,newTable[i]成为了被交换的引用,可疑的语句在于“e.next = newTable[i];”。假设链表e->f->g被t1处理成e<-f<-g,newTable[i]指向了g,这时t2进来了,它一执行“e.next = newTable[i];”就使得e->g,造成了死循环。所以,理论上来说,死循环的Entry个数可能很多。尽管产生了死循环,但是t1执行到了死循环的右边,所以是会继续执行下去的,而t2如果执行“final Entry next = e.next;”的next为null,则也会继续执行下去,否则就进入了死循环。

3)似乎情况会更复杂,因为即便线程跳出了死循环,它下一次做resize进入transfer时,有可能因为之前的死循环Entry链表而被hang住(似乎是一定会被hang住)。也有可能,在put检查Entry链表时(@标记1),因为Entry链表的死循环而被hang住。也似乎有可能,活着的线程和死循环的线程同时执行在while里后,两个线程都能活着出去。所以,可能两个线程平安退出,可能一个线程hang在transfer中,可能两个线程都被hang住而又不一定在一个地方。

4)我反复的测试,出现一个线程被hang住的情况最多,都是e=e.next.next造成的,这主要就是例子put两份增量数据造成的。我如果去掉@标记3的输出,有时也能复现两个线程都被hang住的情况,但加上后就很难复现出来。我又把put的数据改了下,比如让两个线程put范围不同的数据,就能复现出e=e.next,两个线程都被hang住的情况。

上面罗哩罗嗦了很多,一开始我简单的分析后觉得似乎明白了怎么回事,可现在仔细琢磨后似乎又不明白了许多。有一个细节是,每次死循环的key的大小也是有据可循的,我就不打哈了。感觉,如果样本多些,可能出现问题的原因点会很多,也会更复杂,我姑且不再蛋疼下去。至于有人提到ConcurrentHashMap也有这个问题,我觉得不大可能,因为它的put操作是加锁的,如果有这个问题就不叫线程安全的Map了。

2、HashMap中Value可以相同,但是键不可以相同

当插入HashMap的key相同时,会覆盖原有的Value,且返回原Value值,看下面的程序:

[java] view plain copy
  1. public class Test { 
  2.  
  3.     public static void main(String[] args) { 
  4.          
  5.         HashMap<String,Integer> map = new HashMap<String,Integer>(); 
  6.  
  7.         //出入两个Value相同的值,没有问题 
  8.         map.put("egg", 1); 
  9.         map.put("niu", 1); 
  10.          
  11.         //插入key相同的值,看返回结果 
  12.         int egg = (Integer) map.put("egg", 3); 
  13.          
  14.         System.out.println(egg);   //输出1 
  15.         System.out.println(map.get("egg"));   //输出3,将原值1覆盖 
  16.         System.out.println(map.get("niu"));   //输出1 
  17.     } 

相同的键会被覆盖,且返回原值。

3、HashMap按值排序

给定一个数组,求出每个数据出现的次数并按照次数的由大到小排列出来。我们选用HashMap来做,key存储数组元素,值存储出现的次数,最后用Collections的sort方法对HashMap的值进行排序。代码如下:

[java] view plain copy
  1. public class Test { 
  2.  
  3.     public static void main(String[] args) { 
  4.  
  5.         int data[] = { 2, 5, 2, 3, 5, 2, 3, 5, 2, 3, 5, 2, 3, 5, 2
  6.                 7, 8, 8, 7, 8, 7, 9, 0 }; 
  7.         Map<Integer, Integer> map = new HashMap<Integer, Integer>(); 
  8.         for (int i : data) { 
  9.             if (map.containsKey(i)) {//判断HashMap里是否存在 
  10.                 map.put(i, map.get(i) + 1);//已存在,值+1 
  11.             } else
  12.                 map.put(i, 1);//不存在,新增 
  13.             } 
  14.         } 
  15.         //map按值排序 
  16.         List<Map.Entry<Integer, Integer>> list = new ArrayList<Map.Entry<Integer, Integer>>( 
  17.                 map.entrySet()); 
  18.         Collections.sort(list, new Comparator<Map.Entry<Integer, Integer>>() { 
  19.             public int compare(Map.Entry<Integer, Integer> o1, 
  20.                     Map.Entry<Integer, Integer> o2) { 
  21.                 return (o2.getValue() - o1.getValue()); 
  22.             } 
  23.         }); 
  24.         for (Map.Entry<Integer, Integer> m : list) { 
  25.             System.out.println(m.getKey() + "-" + m.getValue()); 
  26.         } 
  27.     } 
  28.  

输出:

2-6
5-5
3-4
8-3
7-3
9-1
0-1

0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 弃妃倾世 倾世皇妃 倾世毒妃 倾世妃 倾世王妃 倾世妖颜 倾世医妃 倾世宠妻 倾世笛仙 红颜倾世 倾世嫡女冷王宠妃无度 倾世大鹏无限之神话逆袭 倾世毒妃楚倾瑶和轩辕炙 倾世毒妃楚倾瑶轩辕炙 倾世暖婚首席亿万追妻 瘫痪王爷之倾世妃 仁手邪妃倾世心 倾世公主逆天成凰 倾世公主之逆天成凰 倾世毒妃楚倾瑶 弃妃倾世全文免费阅读全文 倾世医妃元卿凌 倾世医妃惹不起 倾世霸宠帝君大人别太坏 倾世毒妃全文免费阅读 弃妃倾世苏渺 江山策倾世毒妃免费阅读全文 倾世狂妃废材三小姐 神医弃女之帝妃倾世 苏渺帝北羽全文阅读弃妃倾世 凤帝九倾免费看 倾国倾诚 凰权倾天下凤吟霜 大雨倾盆的意思 一抹柔情倾江南目录 造成骨盆前倾的原因 剑倾天地 沧海一飘雪 九倾凤帝 眸倾天下txt下载 倾然以为默 溪俞 毒妃不好惹容倾月