1. 程式人生 > >知道為啥HashMap裡面的陣列size必須是2的次冪?

知道為啥HashMap裡面的陣列size必須是2的次冪?

最近在寫一個簡易的分離鎖的類:

要求:對不同的Key進行hash得到一個Lock,並要求對鎖對映的概率差不多。比如,160個Key,分佈到16個鎖上,大概有10個Key是對映到同一個鎖上的,只要這樣併發效率才會高。

Java程式碼  收藏程式碼
  1. public class SplitReentrantLock {  
  2.     private Lock[] locks;  
  3.     private int LOCK_NUM;  
  4.     public SplitReentrantLock(int lockNum) {  
  5.         super();  
  6.         LOCK_NUM = lockNum;  
  7.         locks = new Lock[LOCK_NUM];  
  8.         for (int i = 0; i < LOCK_NUM; i++) {  
  9.             locks[i] = new ReentrantLock();  
  10.         }  
  11.     }  
  12.     /** 
  13.      * 獲取鎖, 使用HashMap的hash演算法 
  14.      *  
  15.      *  
  16.      * @param key 
  17.      * @return 
  18.      */  
  19.     public Lock getLock(String key) {  
  20.         int
     lockIndex = index(key);  
  21.         return locks[lockIndex];  
  22.     }  
  23.     int index(String key) {  
  24.         int hash = hash(key.hashCode());          
  25.         return hash & (LOCK_NUM - 1);  
  26.     }  
  27.     int hash(int h) {  
  28.         h ^= (h >>> 20) ^ (h >>> 12);  
  29.         return h ^ (h >>> 7
    ) ^ (h >>> 4);  
  30.     }  

用法:

Java程式碼  收藏程式碼
  1. SplitReentrantLock locks = new SplitReentrantLock(16);  
  2.   Lock lock =locks.getLock(key);   
  3.   lock.lock();  
  4.   try{  
  5.      //......  
  6.    }finally{  
  7.    lock.unlock();   
  8.    }  

本來認為用HashMap的hash演算法就能夠將 達到上述的要求,結果測試的時候嚇了一跳。

測試程式碼:

Java程式碼  收藏程式碼
  1. public class SplitReenterLockTest extends TestCase {  
  2.     public void method(int lockNum, int testNum) {  
  3.         SplitReentrantLock splitLock = new SplitReentrantLock(lockNum);  
  4.         Map<Integer, Integer> map = new TreeMap<Integer, Integer>();  
  5.         for (int i = 0; i < lockNum; i++) {  
  6.             map.put(i, 0);  
  7.         }  
  8.         for (int i = 0; i < testNum; i++) {  
  9.             Integer key = splitLock.index(RandomStringUtils.random(128));  
  10.             map.put(key, map.get(key) + 1);  
  11.         }  
  12.         for (Map.Entry<Integer, Integer> entry : map.entrySet()) {  
  13.             System.out.println(entry.getKey() + " : " + entry.getValue());  
  14.         }  
  15.     }  
  16.     public void test1() {  
  17.         method(501000);}  
  18. }  

結果:1000個隨機key的hash只是對映到8個 Lock上,而不是平均到50個Lock上。

而且是固定分佈到0,1,16,17,32,33,48,49的陣列下標對應的Lock上面,這是為什麼呢?

如果改為:

Java程式碼  收藏程式碼
  1. public void test1() {  
  2.     method(321000);  
  3. }  

 結果:1000個隨機key的hash 對映到32個Lock上,而且基本上是平均分佈的。

問題 :為什麼50和32的hash的效果差別那麼大呢?

再次測試2,4,8,16,64,128. 發現基本上都是平均分佈到所有的Lock上面。

得到平均分佈的這些數都是2的次冪,難道hash演算法和二進位制有關?

看看hash演算法:   

Java程式碼  收藏程式碼
  1.   int index(String key) {  
  2.     int hash = hash(key.hashCode());          
  3.     return hash & (LOCK_NUM - 1);  
  4. }  
  5. int hash(int h) {  
  6.     h ^= (h >>> 20) ^ (h >>> 12);  
  7.     return h ^ (h >>> 7) ^ (h >>> 4);  
  8. }  

 先是經過神奇的(ps:不知道為什麼這麼運算,無知的我只能用神奇來形容)的位運算,最後和LOCK_NUM - 1來進行與運算。

本帖的關鍵點就是在於這個與運算中,如果要想運算後的結果是否平均分佈,在於LOCK_NUM-1的二進位制中1的位數有幾個。如果都是1,那麼肯定是平均分佈到0至LOCK_NUM-1上面。否則僅僅分佈指定的幾位。

下面以50和32說明:

假設Key進行hash執行得到hash值為h,

比如:我測試的資料中的一些h的二進位制值:

Java程式碼  收藏程式碼
  1. 1100000010000110110101010001001  
  2. 10111100001001110111000100010001  
  3. 11111011111010101010000111001001  
  4. 11001010011000100110110111011111  
  5. 10001010100010111101011010011110  

 50的二進位制值:110010.減去1後的二進位制:110001

 32的二進位制值:  100000.減去1後的二進位制:11111

因此h和 49 (即110001)與的結果只能為

000000  : 0

000001  : 1

010000  : 16

010001  : 17

100000  : 32

100001  : 33

110000  : 48

110001  : 49

而h和31 (即11111)與的結果為:

00000

00001

00010

....

11110

11111

這下知道原因了吧。LOCK_NUM -1 二進位制中為1的位數越多,那麼分佈就平均。

這也就是為什麼HashMap預設大小為2的次冪,並且新增元素時,如果超過了一定的數量,那麼就將數量增大到原來的兩倍,其中非常重要的原因就是為了hash的平均分佈 。