自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

空間預(yù)分配思想提升 HashMap 插入效率

開發(fā)
近期線上排查看到用HashMap容量設(shè)置不當(dāng)導(dǎo)致的性能瓶頸,遂以此文簡單介紹一下HashMap容量預(yù)初始化思路和原理,希望對你有幫助。

HashMap容量初始化

都知道HashMap底層是由數(shù)組構(gòu)成,理論上我們顯示聲明HashMap容量為n時,其底層數(shù)組長度應(yīng)該也為n,實際并非如此,HashMap在初始化時會基于我們提供的容量n得到一個2的次方的上限閾值,舉個例子,假如我們初始化HashMap的代碼為new HashMap<>(15),按照HashMap底層的算法,它首先會基于我們的容量減去1即得到14,也就是二進制1110。 然后開始基于當(dāng)前二進制的值進行移位運算然后和1110進行亦或,這里我們以>>>1運算為例,可以看到減去1的結(jié)果為14,通過無符號右移位后得到0111,然后進行按位或運算得到15。

經(jīng)過不斷的無符號右移結(jié)合亦或運算,保證了n的低位都是1:

最后基于這個全1的二進制值再加上1,由此得到一個2的次方的二進制,也就是16,也就是2的4次方:

查看HashMap可以印證這段邏輯,可以看到基于我們的初始化設(shè)置的容量initialCapacity會調(diào)用tableSizeFor獲得一個2的次方的閾值:

public HashMap(int initialCapacity, float loadFactor) {
       //......
        this.loadFactor = loadFactor;//默認(rèn)為0.75f
        this.threshold = tableSizeFor(initialCapacity);
    }

步入tableSizeFor即可看到筆者上文所說的,先進行無符號右移在進行按位或運算,得到一個低位為全1的二進制數(shù),然后再加上1,得到一個2的次方的上限閾值,由此完成HashMap初始化的第一步,我們得到的上限閾值為16,負(fù)載因子取默認(rèn)值即0.75:

static final int tableSizeFor(int cap) {
  //容量減1
        int n = cap - 1;
        //基于n進行無符號右移得到低位包含1的字段,進行按位或保證低位盡可能都為1
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        //基于計算結(jié)果加上1,得到2的次方
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap插入細(xì)節(jié)

接下來就是插入操作的細(xì)節(jié),HashMap初次進行插入會對數(shù)組容量進行初始化,對應(yīng)的加載公式為上限閾值*負(fù)載因子,也就是16*0.75按照整形計算方式,最終得到值為12,對此我們給出入口代碼putVal,可以看到如果看到底層數(shù)組為空則會調(diào)用resize進行初始化:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //初次插入會調(diào)用resize初始化底層數(shù)組
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
       
           //......
        return null;
    }

步入resize即可看到,初次初始化會基于上限閾值和負(fù)載因子相乘然后得到一個取整的2的次方的容量的數(shù)組:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //oldThr 設(shè)置為當(dāng)前閾值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //......
        //初始化容量為上限閾值的值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //......
        //默認(rèn)情況下新的閾值為0,于是步入該邏輯,將容量設(shè)置為上限閾值*負(fù)載因子然后取整
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
       //......
    }

那么問題也就來了,我們愿意是15時進行擴容,按照這套邏輯,我們?nèi)萘窟_到12時就會進行擴容,默認(rèn)情況下HashMap的擴容都是2倍擴容,在極端場景情況下,這種開銷是非常大的:

HashMap的容量設(shè)置多少合適

解決錯誤擴容時機的本質(zhì)原因就是初次put操作*0.75,對此我們可以提前基于當(dāng)前容量/0.75即可。實際上guava包的newHashMapWithExpectedSize方法就做到了這一點。該方法其底層容量計算公式為(int) ((float) expectedSize / 0.75F + 1.0F),即基于當(dāng)前容量除0.75再加1,由此抵消put操作初始化時*0.75的傷害:

對此我們給出guava包的Maps工具下的newHashMapWithExpectedSize的源代碼:

public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
 //調(diào)用capacity獲得可以抵消傷害的容量
    return new HashMap<>(capacity(expectedSize));
  }


  static int capacity(int expectedSize) {
    //......
    if (expectedSize < Ints.MAX_POWER_OF_TWO) {
    //提前/0.75+1進行預(yù)分配,用于抵消put操作的縮容
      return (int) ((float) expectedSize / 0.75F + 1.0F);
    }
    return Integer.MAX_VALUE; // any large value
  }

壓測

我們以原生和guava工具包Maps對應(yīng)的newHashMapWithExpectedSize分別創(chuàng)建2的24次方的容量空間,然后進行鍵值對插入操作:

public class Main {
    private static int SIZE = 1 << 24;

    public static void main(String[] args) {

        HashMap<Integer, Integer> map = new HashMap<>(SIZE);
        HashMap<Integer, Integer> map2 = Maps.newHashMapWithExpectedSize(SIZE);
        batchSave(map);
        batchSave(map2);


    }

    private static void batchSave(HashMap<Integer, Integer> map) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < SIZE; i++) {
            map.put(i, i);
        }
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
    }
  
}

可以看到,使用guava的API操作時間整整少了7s左右:

22821ms
15831ms
責(zé)任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關(guān)推薦

2009-08-22 21:09:02

改變預(yù)分配硬盤空間

2013-05-21 09:09:49

服務(wù)器虛擬化網(wǎng)卡

2013-05-21 09:08:24

服務(wù)器虛擬化網(wǎng)卡

2014-04-28 10:17:01

2011-07-08 10:22:12

智能布線

2024-10-09 12:18:38

2014-12-26 10:58:35

托管云托管私有云公共云

2015-01-06 09:59:03

2009-03-11 17:31:46

2024-08-30 17:14:34

2015-07-28 10:42:34

DevOpsIT效率

2012-07-20 14:44:17

外網(wǎng)建設(shè)H3C

2009-06-28 22:55:00

SAN惠普存儲

2021-04-29 08:13:49

Mac 工具軟件

2024-08-19 00:40:00

SQL數(shù)據(jù)庫

2018-05-05 09:00:40

生產(chǎn)效率

2021-06-21 11:05:30

CSS前端代碼
點贊
收藏

51CTO技術(shù)棧公眾號