【集合之HashMap】HashMap实现原理及非线程安全原因

要知道HashMap是什么,首先要搞清楚它的数据结构,在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“)。

从图中我们可以看到一个HashMap就是一个数组结构,当新建一个HashMap的时候,就会初始化一个数组。

 

HashMap源码中的3个常量,

 1     /**
 2      * The default initial capacity - MUST be a power of two.
 3      */
 4     static final int DEFAULT_INITIAL_CAPACITY = 16;
 5 
 6     /**
 7      * The maximum capacity, used if a higher value is implicitly specified
 8      * by either of the constructors with arguments.
 9      * MUST be a power of two <= 1<<30.
10      */
11     static final int MAXIMUM_CAPACITY = 1 << 30;
12 
13     /**
14      * The load factor used when none specified in constructor.
15      */
16     static final float DEFAULT_LOAD_FACTOR = 0.75f;
View Code

这三个常量其实是对应HashMap的三个性能参数的,这三个参数的意义如下:

1、DEFAULT_INITIAL_CAPACITY初始值为16。该初始值可以外部传参数指定,必须为2的整数次幂。如果外部指定的容量参数大于MAXIMUM_CAPACITY,则会按MAXIMUM_CAPACITY来处理,否则会计算一个大于该参数并最接近该参数的2的整数次幂最为HashMap的初始容量。

2、MAXIMUM_CAPACITY是HashMap中元素容量的最大值(1<<30)。

3、DEFAULT_LOAD_FACTOR是默认的hash因子(0.75f),可以外部指定,必须小于1.0f,否则不能rehash。

 

HashMap的put操作,

 1 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     }
15 
16     modCount++;
17     addEntry(hash, key, value, i);
18     return null;
19 }
20 
21 static int hash(int h) {
22     // This function ensures that hashCodes that differ only by
23     // constant multiples at each bit position have a bounded
24     // number of collisions (approximately 8 at default load factor).
25     h ^= (h >>> 20) ^ (h >>> 12);
26     return h ^ (h >>> 7) ^ (h >>> 4);
27 }
28 
29 static int indexFor(int h, int length) {
30     return h & (length-1);
31 }
32 
33 void addEntry(int hash, K key, V value, int bucketIndex) {
34     Entry<K,V> e = table[bucketIndex];
35     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
36     if (size++ >= threshold)
37         resize(2 * table.length);
38 }
39 
40 void resize(int newCapacity) {
41     Entry[] oldTable = table;
42     int oldCapacity = oldTable.length;
43     if (oldCapacity == MAXIMUM_CAPACITY) {
44         threshold = Integer.MAX_VALUE;
45         return;
46     }
47 
48     Entry[] newTable = new Entry[newCapacity];
49     transfer(newTable);
50     table = newTable;
51     threshold = (int)(newCapacity * loadFactor);
52 }
53 
54 void transfer(Entry[] newTable) {
55     Entry[] src = table;
56     int newCapacity = newTable.length;
57     for (int j = 0; j < src.length; j++) {
58         Entry<K,V> e = src[j];
59         if (e != null) {
60             src[j] = null;
61             do {
62                 Entry<K,V> next = e.next;
63                 int i = indexFor(e.hash, newCapacity);
64                 e.next = newTable[i];
65                 newTable[i] = e;
66                 e = next;
67             } while (e != null);
68         }
69     }
70 }
View Code

从上面这段代码可以看出HashMap的put操作具体处理过程如下:

1、取出key的hashcode,调用hash函数来将hashcode高地位异或成一个新的值,这个hash函数是数学上的扰动函数,可以达到比较好的解决hash冲突的效果。

2、indexFor方法中将h和length-1做与运算,由于length是2的整数次幂,所以这个函数相当于取模length。

3、根据2中返回的数组位置去遍历该位置对应的链表是否存在该元素,存在的话就用新的value替换旧的value,并返回旧的value,跳出put操作。否则就进行步骤4。

4、modeCount用来记录对HashMap修改的,做一次修改加一,相当于版本号,用于迭代器遍历的过程中同时有人修改了HashMap时抛异常,所以HashMap在迭代器遍历的时候是不允许其他线程修改的。完成了3以后则将进入addEntry方法来将新元素挂载数组对应的链表头部。如果HashMap当前的元素个数不小于capacity * loadFactor,还需要进行rehash(即重新对HashMap做翻天覆地的调整)。

5、rehash的过程其实是将旧的散列链表中的元素全部放到二倍大小的新的散列链表中,transfer方法会将每条链表从头至尾遍历后放入新的位置,放置到新链表的过程会逆原来顺序。

 

HashMap的get、remove相对比较简单就是遍历散列链表的过程,putAll操作是首先用参数中的map的size对当前HashMap做一次rehash判断。

 

HashMap的死循环,主要发生在多线程rehash的过程中,又有线程去get的时候,分析过程如下:

单线程的rehash过程:

 

多线程的rehash过程:

1、假设我们有两个线程。我用红色和浅蓝色标注了一下。

我们再回头看一下我们的 transfer代码中的这个细节:

1 do {
2     Entry<K,V> next = e.next; // 假设线程一执行到这里就被调度挂起了
3     int i = indexFor(e.hash, newCapacity);
4     e.next = newTable[i];
5     newTable[i] = e;
6     e = next;
7 } while (e != null);
View Code

而我们的线程二执行完成了。于是我们有下面的这个样子。

注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转了。

2、线程一被调度回来执行。
先是执行 newTalbe[i] = e;
然后是e = next,导致了e指向了key(7),
而下一次循环的next = e.next导致了next指向了key(3),

 

3、一切安好。
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。

 

4、环形链接出现。
e.next = newTable[i] 导致  key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

 

于是,当我们的线程一调用到第4个数组元素对应的链表时就死循环了。

posted @ 2016-08-20 17:12  右煊虎  阅读(491)  评论(0编辑  收藏  举报