java8: hashmap性能提高

HashMap是一個高效通用的數據結構,它在每個Java程序中都隨處可見。先來介紹些基礎知識。你可能也知道,HashMap使用key的hashCode()和equals()方法來將值劃分到不一樣的桶裏。桶的數量一般要比map中的記錄的數量要稍大,這樣每一個桶包括的值會比較少(最好是一個)。當經過key進行查找時,咱們能夠在常數時間內迅速定位到某個桶(使用hashCode()對桶的數量進行取模)以及要找的對象。算法

這些東西你應該都已經知道了。你可能還知道哈希碰撞會對hashMap的性能帶來災難性的影響。若是多個hashCode()的值落到同一個桶內的時候,這些值是存儲到一個鏈表中的。最壞的狀況下,全部的key都映射到同一個桶中,這樣hashmap就退化成了一個鏈表——查找時間從O(1)到O(n)。咱們先來測試下正常狀況下hashmap在Java 7和Java 8中的表現。爲了能完成控制hashCode()方法的行爲,咱們定義了以下的一個Key類:緩存

class Key implements Comparable<Key> {private final int value;Key(int value) {this.value = value;}@Overridepublic int compareTo(Key o) {return Integer.compare(this.value, o.value);}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass())return false;Key key = (Key) o;return value == key.value;}@Overridepublic int hashCode() {return value;}}

Key類的實現中規中矩:它重寫了equals()方法而且提供了一個還算過得去的hashCode()方法。爲了不過分的GC,我將不可變的Key對象緩存了起來,而不是每次都從新開始建立一遍:服務器

class Key implements Comparable<Key> {public class Keys {public static final int MAX_KEY = 10_000_000;private static final Key[] KEYS_CACHE = new Key[MAX_KEY];static {for (int i = 0; i < MAX_KEY; ++i) {KEYS_CACHE[i] = new Key(i);}}public static Key of(int value) {return KEYS_CACHE[value];}}

如今咱們能夠開始進行測試了。咱們的基準測試使用連續的Key值來建立了不一樣的大小的HashMap(10的乘方,從1到1百萬)。在測試中咱們還會使用key來進行查找,並測量不一樣大小的HashMap所花費的時間:數據結構

import com.google.caliper.Param;import com.google.caliper.Runner;import com.google.caliper.SimpleBenchmark;public class MapBenchmark extends SimpleBenchmark {private HashMap<Key, Integer> map;@Paramprivate int mapSize;@Overrideprotected void setUp() throws Exception {map = new HashMap<>(mapSize);for (int i = 0; i < mapSize; ++i) {map.put(Keys.of(i), i);}}public void timeMapGet(int reps) {for (int i = 0; i < reps; i++) {map.get(Keys.of(i % mapSize));}}}

有意思的是這個簡單的HashMap.get()裏面,Java 8比Java 7要快20%。總體的性能也至關不錯:儘管HashMap裏有一百萬條記錄,單個查詢也只花了不到10納秒,也就是大概我機器上的大概20個CPU週期。至關使人震撼!不過這並非咱們想要測量的目標。ide

假設有一個不好勁的key,他老是返回同一個值。這是最糟糕的場景了,這種狀況徹底就不該該使用HashMap:性能

class Key implements Comparable<Key> {//...@Overridepublic int hashCode() {return 0;}}

Java 7的結果是預料中的。隨着HashMap的大小的增加,get()方法的開銷也愈來愈大。因爲全部的記錄都在同一個桶裏的超長鏈表內,平均查詢一條記錄就須要遍歷一半的列表。所以從圖上能夠看到,它的時間複雜度是O(n)。測試

不過Java 8的表現要好許多!它是一個log的曲線,所以它的性能要好上好幾個數量級。儘管有嚴重的哈希碰撞,已經是最壞的狀況了,但這個一樣的基準測試在JDK8中的時間複雜度是O(logn)。單獨來看JDK 8的曲線的話會更清楚,這是一個對數線性分佈:優化

爲何會有這麼大的性能提高,儘管這裏用的是大O符號(大O描述的是漸近上界)?其實這個優化在JEP-180中已經提到了。若是某個桶中的記錄過大的話(當前是TREEIFY_THRESHOLD = 8),HashMap會動態的使用一個專門的treemap實現來替換掉它。這樣作的結果會更好,是O(logn),而不是糟糕的O(n)。它是如何工做的?前面產生衝突的那些KEY對應的記錄只是簡單的追加到一個鏈表後面,這些記錄只能經過遍從來進行查找。可是超過這個閾值後HashMap開始將列表升級成一個二叉樹,使用哈希值做爲樹的分支變量,若是兩個哈希值不等,但指向同一個桶的話,較大的那個會插入到右子樹裏。若是哈希值相等,HashMap但願key值最好是實現了Comparable接口的,這樣它能夠按照順序來進行插入。這對HashMap的key來講並非必須的,不過若是實現了固然最好。若是沒有實現這個接口,在出現嚴重的哈希碰撞的時候,你就並別期望能得到性能提高了。this

這個性能提高有什麼用處?比方說惡意的程序,若是它知道咱們用的是哈希算法,它可能會發送大量的請求,致使產生嚴重的哈希碰撞。而後不停的訪問這些key就能顯著的影響服務器的性能,這樣就造成了一次拒絕服務攻擊(DoS)。JDK 8中從O(n)到O(logn)的飛躍,能夠有效地防止相似的攻擊,同時也讓HashMap性能的可預測性稍微加強了一些。我但願這個提高能最終說服你的老大讚成升級到JDK 8來。google

測試使用的環境是:Intel Core i7-3635QM @ 2.4 GHz,8GB內存,SSD硬盤,使用默認的JVM參數,運行在64位的Windows 8.1系統 上。

相關文章
相關標籤/搜索