1. 程式人生 > >曹工說JDK原始碼(3)--ConcurrentHashMap,Hash演算法優化、位運算揭祕

曹工說JDK原始碼(3)--ConcurrentHashMap,Hash演算法優化、位運算揭祕

# hashcode,有點講究 什麼是好的hashcode,一般來說,一個hashcode,一般用int來表示,32位。 下面兩個hashcode,大家覺得怎麼樣? ```java 0111 1111 1111 1111 1111 1111 1111 1111 ------A 1111 1111 1111 1111 1111 1111 1111 1111 ------B ``` 只有第32位(從右到左)不一樣,好像也沒有所謂的好壞吧? 那,我們再想想,hashcode一般怎麼使用呢?在hashmap中,由陣列+連結串列+紅黑樹組成,其中,陣列乃重中之重,假設陣列長度為2的n次方,(hashmap的陣列,強制要求長度為2的n次方),這裡假設為8. 大家又知道,hashcode 對 8 取模,效果等同於 hashcode & (8 - 1)。 那麼,前面的A 和 (8 - 1)相與的結果如何呢? ```java 0111 1111 1111 1111 1111 1111 1111 1111 ------A 0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1 相與 0000 0000 0000 0000 0000 0000 0000 0111 ------ 7 ``` 結果為7,也就是,會放進array[7]。 大家再看B的計算過程: ```java 1111 1111 1111 1111 1111 1111 1111 1111 ------B 0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1 相與 0000 0000 0000 0000 0000 0000 0000 0111 ------ 7 ``` 雖然B的第32位為1,但是,奈何和我們相與的隊友,7,是個垃圾。 前面的高位,全是0。 ok,你懂了嗎,陣列長度太小了,才8,導致前面有29位都是0;你可能覺得一般容量不可能這麼小,那假設容量為2的16次方,容量為65536,這下不是很小了吧,但即使如此,前面的16位也是0. 所以,問題明白了嗎,我們計算出來的hashcode,低位相同,高位不同;但是,因為和我們進行`與`計算的隊友太過垃圾,導致我們出現了hash衝突。 ok,我們怎麼來解決這個問題呢? 我們能不能把高位也參與計算呢?自然,是可以的。 # hashmap中如何優化 ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 這裡,其實分了3個步驟: 1. 計算hashcode,作為運算元1 ```java h = key.hashCode() ``` 2. 將第一步的hashcode,右移16位,作為運算元2 ```java h >>> 16 ``` 3. 運算元1 和 運算元2 進行異或操作,得到最終的hashcode 還是拿前面的來算, ```java 0111 1111 1111 1111 1111 1111 1111 1111 ------A 0000 0000 0000 0000 0111 1111 1111 1111 ----- A >>> 16 異或(相同則為0,否則為1) 0111 1111 1111 1111 1000 0000 0000 0000 --- 2147450880 ``` 這裡算出來的結果是 2147450880,再去對 7 進行與運算: ```java 0111 1111 1111 1111 1000 0000 0000 0000 --- 2147450880 0000 0000 0000 0000 0000 0000 0000 0111 ------ 8 -1 與運算 0000 0000 0000 0000 0000 0000 0000 0000 ------ 0 ``` 這裡的A,算出來,依然在array[0]。 再拿B來算一下: ```java 1111 1111 1111 1111 1111 1111 1111 1111 ------ B 0000 0000 0000 0000 1111 1111 1111 1111 ----- B >>> 16 異或(相同則為0,否則為1) 1111 1111 1111 1111 0000 0000 0000 0000 --- -65536 0000 0000 0000 0000 0000 0000 0000 0111 ------ 7 與運算 0000 0000 0000 0000 0000 0000 0000 0000 ------- 0 ``` 最終算出來為0,所以,應該放在array[0]。 恩?算出來兩個還是衝突了,我只能說,我挑的數字真的牛逼,是不是該去買彩票啊。。 總的來說,大家可以多試幾組數,下邊提供下原始碼: ```java public class BinaryTest { public static void main(String[] args) { int a = 0b00001111111111111111111111111011; int b = 0b10001101111111111111110111111011; int i = tabAt(32, a); System.out.println("index for a:" + i); i = tabAt(32, b); System.out.println("index for b:" + i); } static final int tabAt(int arraySize, int hash) { int h = hash; int finalHashCode = h ^ (h >>> 16); int i = finalHashCode & (arraySize - 1); return i; } } ``` 雖然說,我測試了幾個數字,還是有些衝突,但是,你把高16位弄進來參與計算,總比你不弄進來計算要好吧。 大家也可以看看hashmap中,hash方法的註釋: > ``` > /** > * Computes key.hashCode() and spreads (XORs) higher bits of hash > * to lower. Because the table uses power-of-two masking, sets of > * hashes that vary only in bits above the current mask will > * always collide. (Among known examples are sets of Float keys > * holding consecutive whole numbers in small tables.) So we > * apply a transform that spreads the impact of higher bits > * downward. There is a tradeoff between speed, utility, and > * quality of bit-spreading. Because many common sets of hashes > * are already reasonably distributed (so don't benefit from > * spreading), and because we use trees to handle large sets of > * collisions in bins, we just XOR some shifted bits in the > * cheapest possible way to reduce systematic lossage, as well as > * to incorporate impact of the highest bits that would otherwise > * never be used in index calculations because of table bounds. > */ > ``` 裡面提到了2點: > So we apply a transform that spreads the impact of higher bits downward. 所以,我們進行了一個轉換,把高位的作用利用起來。 > we just XOR some shifted bits in the cheapest possible way to reduce systematic lossage, as well as > > to incorporate impact of the highest bits that would otherwise never be used in index calculations because of table bounds. 我們僅僅異或了從高位移動下來的二進位制位,用最經濟的方式,削減系統性能損失,同樣,因為陣列大小的限制,導致高位在索引計算中一直用不到,我們通過這種轉換將其利用起來。 # ConcurrentHashMap如何優化 在concurrentHashMap中,其主要是: ```java final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); int hash = spread(key.hashCode()); ``` 這裡主要是使用spread方法來計算hash值: ```java static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; } ``` 大家如果要仔細觀察每一步的二進位制,可以使用下面的demo: ```java static final int spread(int h) { // 1 String s = Integer.toBinaryString(h); System.out.println("h:" + s); // 2 String lower16Bits = Integer.toBinaryString(h >>> 16); System.out.println("lower16Bits:" + lower16Bits); // 3 int temp = h ^ (h >>> 16); System.out.println("h ^ (h >>> 16):" + Integer.toBinaryString(temp)); // 4 int result = (temp) & HASH_BITS; System.out.println("final:" + Integer.toBinaryString(result)); return result; } ``` 這裡和HashMap相比,多了點東西,也就是多出來了: `& HASH_BITS;` 這個有什麼用處呢? 因為`(h ^ (h >>> 16))`計算出來的hashcode,可能是負數。這裡,和 HASH_BITS進行了相與: ```java static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash ``` ```java 1111 1111 1111 1111 1111 1111 1111 1111 假設計算出來的hashcode為負數,因為第32位為1 0111 1111 1111 1111 1111 1111 1111 1111 0x7fffffff 進行相與 0111 .................................. ``` ​ 這裡,第32位,因為0x7fffffff的第32位,總為0,所以相與後的結果,第32位也總為0 ,所以,這樣的話,hashcode就總是正數了,不會是負數。 # concurrentHashMap中,node的hashcode,為啥不能是負數 當hashcode為正數時,表示該雜湊桶為正常的連結串列結構。 當hashcode為負數時,有幾種情況: ## ForwardingNode 此時,其hash值為: ```java static final int MOVED = -1; // hash for forwarding nodes ``` 當節點為ForwardingNode型別時(表示雜湊表在擴容進行中,該雜湊桶已經被遷移到了新的臨時hash表,此時,要get的話,需要去臨時hash表查詢;要put的話,是不行的,會幫助擴容) ## TreeBin ```java static final int TREEBIN = -2; // hash for roots of trees ``` 表示,該雜湊桶,已經轉了紅黑樹。 # 擴容時的位運算 ```java /** * Returns the stamp bits for resizing a table of size n. * Must be negative when shifted left by RESIZE_STAMP_SHIFT. */ static final int resizeStamp(int n) { return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1)); } ``` 這裡,假設,n為4,即,hashmap中陣列容量為4. * 下面這句,求4的二進位制表示中,前面有多少個0. Integer.numberOfLeadingZeros(n) 表示為32位後,如下 0000 0000 0000 0000, 0000 0000 0000 0100 所以,前面有29個0,即,這裡的結果為29. * (1 << (RESIZE_STAMP_BITS - 1) 這一句呢,其中RESIZE_STAMP_BITS 是個常量,為16. 相當於,把1 向左移動15位。 二進位制為: ```java 1000 0000 0000 0000 -- 1 << 15 ``` 最終結果: ```java 0000 0000 0000 0000 0000 0000 0001 1101 -- 29 0000 0000 0000 0000 1000 0000 0000 0000 -- 1 << 15 進行或運算 0000 0000 0000 0000 1000 0000 0001 1101 -- 相當於把29的第一位,變成了1,其他都沒變。 ``` 所以,最終結果是, ![](https://img2020.cnblogs.com/blog/519126/202006/519126-20200608223421002-460567379.png) 這個數,換算為10進位制,為32972,是個正數。 這個數,有啥用呢? 在addCount函式中,當整個雜湊表的鍵值對數量,超過sizeCtl時(一般為0.75 * 陣列長度),就會觸發擴容。 ```java java.util.concurrent.ConcurrentHashMap#addCount int sc = sizeCtl; boolean bSumExteedSizeControl = newBaseCount >
= (long) sc; // 1 if (bContinue) { int rs = resizeStamp(n); // 2 if (sc < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } // 3 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); newBaseCount = sumCount(); } else { break; } ``` * 1處,如果擴容條件滿足 * 2處,如果sc小於0,這個sc是啥,就是前面說的sizeCtl,此時應該是等於:0.75 * 陣列長度,不可能為負數 * 3處,將sc(此時為正數),cas修改為: ```java (rs << RESIZE_STAMP_SHIFT) + 2) ``` 這個數有點意思了,rs就是前面我們的resizeStamp得到的結果。 按照前面的demo,我們拿到的結果為: ```java 0000 0000 0000 0000 1000 0000 0001 1101 -- 相當於把29的第一位,變成了1,其他都沒變。 ``` 因為 ```java private static int RESIZE_STAMP_BITS = 16; private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; ``` 所以,RESIZE_STAMP_SHIFT 為16. ```java 0000 0000 0000 0000 1000 0000 0001 1101 -- 相當於把29的第一位,變成了1,其他都沒變。 1000 0000 0001 1101 0000 0000 0000 0000 --- 左移16位,即 rs << RESIZE_STAMP_SHIFT 1000 0000 0001 1101 0000 0000 0000 0010 -- (rs << RESIZE_STAMP_SHIFT) + 2) ``` 最終,這個數,第一位是 1,說明了,這個數,肯定是負數。 大家如果看過其他人寫的資料,也就知道,當sizeCtl為負數時,表示正在擴容。 所以,這裡 ```java if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) ``` 這句話就是,如果當前執行緒成功地,利用cas,將sizeCtl從正數,變成負數,就可以進行擴容。 ##擴容時,其他執行緒怎麼執行 ```java // 1 if (bContinue) { int rs = resizeStamp(n); // 2 if (sc < 0) { // 2.1 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0) break; // 2.2 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } // 3 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); newBaseCount = sumCount(); } else { break; } ``` 此時,因為上面的執行緒觸發了擴容,sc已經變成了負數了,此時,新的執行緒進來,會判斷2處。 2處是滿足的,會進入2.1處判斷,這裡的部分條件看不懂,大概是:擴容已經結束,就不再執行,直接break 否則,進入2.2處,輔助擴容,同時,把sc變成sc + 1,增加擴容執行緒數。 # 總結 時間倉促,如有問題,歡迎