知道為啥HashMap裡面的陣列size必須是2的次冪?
最近在寫一個簡易的分離鎖的類:
要求:對不同的Key進行hash得到一個Lock,並要求對鎖對映的概率差不多。比如,160個Key,分佈到16個鎖上,大概有10個Key是對映到同一個鎖上的,只要這樣併發效率才會高。
Java程式碼- public class SplitReentrantLock {
- private Lock[] locks;
- private int LOCK_NUM;
- public SplitReentrantLock(int lockNum) {
- super();
-
LOCK_NUM = lockNum;
- locks = new Lock[LOCK_NUM];
- for (int i = 0; i < LOCK_NUM; i++) {
- locks[i] = new ReentrantLock();
- }
- }
- /**
- * 獲取鎖, 使用HashMap的hash演算法
- *
- *
- * @param key
- * @return
- */
- public Lock getLock(String key) {
-
int
- return locks[lockIndex];
- }
- int index(String key) {
- int hash = hash(key.hashCode());
- return hash & (LOCK_NUM - 1);
- }
- int hash(int h) {
- h ^= (h >>> 20) ^ (h >>> 12);
-
return h ^ (h >>> 7
- }
用法:
Java程式碼- SplitReentrantLock locks = new SplitReentrantLock(16);
- Lock lock =locks.getLock(key);
- lock.lock();
- try{
- //......
- }finally{
- lock.unlock();
- }
本來認為用HashMap的hash演算法就能夠將 達到上述的要求,結果測試的時候嚇了一跳。
測試程式碼:
Java程式碼- public class SplitReenterLockTest extends TestCase {
- public void method(int lockNum, int testNum) {
- SplitReentrantLock splitLock = new SplitReentrantLock(lockNum);
- Map<Integer, Integer> map = new TreeMap<Integer, Integer>();
- for (int i = 0; i < lockNum; i++) {
- map.put(i, 0);
- }
- for (int i = 0; i < testNum; i++) {
- Integer key = splitLock.index(RandomStringUtils.random(128));
- map.put(key, map.get(key) + 1);
- }
- for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
- System.out.println(entry.getKey() + " : " + entry.getValue());
- }
- }
- public void test1() {
- method(50, 1000);}
- }
結果:1000個隨機key的hash只是對映到8個 Lock上,而不是平均到50個Lock上。
而且是固定分佈到0,1,16,17,32,33,48,49的陣列下標對應的Lock上面,這是為什麼呢?
如果改為:
Java程式碼- public void test1() {
- method(32, 1000);
- }
結果:1000個隨機key的hash 對映到32個Lock上,而且基本上是平均分佈的。
問題 :為什麼50和32的hash的效果差別那麼大呢?
再次測試2,4,8,16,64,128. 發現基本上都是平均分佈到所有的Lock上面。
得到平均分佈的這些數都是2的次冪,難道hash演算法和二進位制有關?
看看hash演算法:
Java程式碼- int index(String key) {
- int hash = hash(key.hashCode());
- return hash & (LOCK_NUM - 1);
- }
- int hash(int h) {
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
先是經過神奇的(ps:不知道為什麼這麼運算,無知的我只能用神奇來形容)的位運算,最後和LOCK_NUM - 1來進行與運算。
本帖的關鍵點就是在於這個與運算中,如果要想運算後的結果是否平均分佈,在於LOCK_NUM-1的二進位制中1的位數有幾個。如果都是1,那麼肯定是平均分佈到0至LOCK_NUM-1上面。否則僅僅分佈指定的幾位。
下面以50和32說明:
假設Key進行hash執行得到hash值為h,
比如:我測試的資料中的一些h的二進位制值:
Java程式碼- 1100000010000110110101010001001
- 10111100001001110111000100010001
- 11111011111010101010000111001001
- 11001010011000100110110111011111
- 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的平均分佈 。