zoukankan      html  css  js  c++  java
  • [Java集合] 彻底搞懂HashMap,HashTable,ConcurrentHashMap之关联.

    注: 今天看到的一篇讲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

查看全文
  • 相关阅读:
    407 Trapping Rain Water II 接雨水 II
    406 Queue Reconstruction by Height 根据身高重建队列
    405 Convert a Number to Hexadecimal 数字转换为十六进制数
    404 Sum of Left Leaves 左叶子之和
    403 Frog Jump 青蛙过河
    402 Remove K Digits 移掉K位数字
    401 Binary Watch 二进制手表
    400 Nth Digit 第N个数字
    398 Random Pick Index 随机数索引
    397 Integer Replacement 整数替换
  • 原文地址:https://www.cnblogs.com/wang-meng/p/5808006.html
  • Copyright © 2011-2022 走看看