日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

HashMap深入分析

 燮羽 2010-12-14

java.util.HashMap是很常見(jiàn)的類,前段時(shí)間公司系統(tǒng)由于對(duì)HashMap使用不當(dāng),導(dǎo)致cpu百分之百,在并發(fā)環(huán)境下使用HashMap而沒(méi)有做同步,可能會(huì)引起死循環(huán),關(guān)于這一點(diǎn),sun的官方網(wǎng)站上已有闡述,這并非是bug。


HashMap的數(shù)據(jù)結(jié)構(gòu)

         HashMap主要是用數(shù)組來(lái)存儲(chǔ)數(shù)據(jù)的,我們都知道它會(huì)對(duì)key進(jìn)行哈希運(yùn)算,哈系運(yùn)算會(huì)有重復(fù)的哈希值,對(duì)于哈希值的沖突,HashMap采用鏈表來(lái)解決的。在HashMap里有這樣的一句屬性聲明:

transient Entry[] table;

Entry就是HashMap存儲(chǔ)的數(shù)據(jù),它擁有的屬性如下

final K key;
 V value;
 final int hash;
 Entry<K,V> next;

看到next了嗎?next就是為了哈希沖突而存在的。比如通過(guò)哈希運(yùn)算,一個(gè)新元素應(yīng)該在數(shù)組的第10個(gè)位置,但是第10個(gè)位置已經(jīng)有Entry,那么好吧,將新加的元素也放到第10個(gè)位置,將第10個(gè)位置的原有Entry賦值給當(dāng)前新加的Entry的next屬性。數(shù)組存儲(chǔ)的鏈表,鏈表是為了解決哈希沖突的,這一點(diǎn)要注意。


幾個(gè)關(guān)鍵的屬性

存儲(chǔ)數(shù)據(jù)的數(shù)組

transient Entry[] table; 這個(gè)上面已經(jīng)講到了
默認(rèn)容量

static final int DEFAULT_INITIAL_CAPACITY = 16;

最大容量

 static final int MAXIMUM_CAPACITY = 1 << 30;

默認(rèn)加載因子,加載因子是一個(gè)比例,當(dāng)HashMap的數(shù)據(jù)大小>=容量*加載因子時(shí),HashMap會(huì)將容量擴(kuò)容

static final float DEFAULT_LOAD_FACTOR = 0.75f;

當(dāng)實(shí)際數(shù)據(jù)大小超過(guò)threshold時(shí),HashMap會(huì)將容量擴(kuò)容,threshold=容量*加載因子

int threshold;

加載因子

final float loadFactor;


HashMap的初始過(guò)程

構(gòu)造函數(shù)1

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        // Find a power of 2 >= initialCapacity
        int capacity = 1;
        while (capacity < initialCapacity) 
            capacity <<= 1;
    
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }

重點(diǎn)注意這里 

while (capacity < initialCapacity) 
            capacity <<= 1;

capacity才是初始容量,而不是initialCapacity,這個(gè)要特別注意,如果執(zhí)行new HashMap(9,0.75);那么HashMap的初始容量是16,而不是9,想想為什么吧。


構(gòu)造函數(shù)2

 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }


構(gòu)造函數(shù)3,全部都是默認(rèn)值

   public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
        table = new Entry[DEFAULT_INITIAL_CAPACITY];
        init();
    }


構(gòu)造函數(shù)4

  public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }



如何哈希

        HashMap并不是直接將對(duì)象的hashcode作為哈希值的,而是要把key的hashcode作一些運(yùn)算以得到最終的哈希值,并且得到的哈希值也不是在數(shù)組中的位置哦,無(wú)論是get還是put還是別的方法,計(jì)算哈希值都是這一句:

int hash = hash(key.hashCode());

hash函數(shù)如下:

  static int hash(int h) {
    return useNewHash ? newHash(h) : oldHash(h);
    }

useNewHash聲明如下:

   private static final boolean useNewHash;
    static { useNewHash = false; }

這說(shuō)明useNewHash其實(shí)一直為false且不可改變的,hash函數(shù)里對(duì)useNewHash的判斷真是多余的。

    private static int oldHash(int h) {
        h += ~(h << 9);
        h ^=  (h >>> 14);
        h +=  (h << 4);
        h ^=  (h >>> 10);
        return h;
    }

    private static int newHash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

其實(shí)HashMap的哈希函數(shù)會(huì)一直都是oldHash。

 

 

如果確定數(shù)據(jù)的位置

看下面兩行

      int hash = hash(k.hashCode());
      int i = indexFor(hash, table.length);

第一行,上面講過(guò)了,是得到哈希值,第二行,則是根據(jù)哈希指計(jì)算元素在數(shù)組中的位置了,位置的計(jì)算是將哈希值和數(shù)組長(zhǎng)度按位與運(yùn)算。

   static int indexFor(int h, int length) {
        return h & (length-1);
    }

 

 

put方法到底作了什么?

   public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

如果key為NULL,則是單獨(dú)處理的,看看putForNullKey方法:

    private V putForNullKey(V value) {
        int hash = hash(NULL_KEY.hashCode());
        int i = indexFor(hash, table.length);

        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            if (e.key == NULL_KEY) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, (K) NULL_KEY, value, i);
        return null;
    }

NULL_KEY的聲明:static final Object NULL_KEY = new Object();

這一段代碼是處理哈希沖突的,就是說(shuō),在數(shù)組某個(gè)位置的對(duì)象可能并不是唯一的,它是一個(gè)鏈表結(jié)構(gòu),根據(jù)哈希值找到鏈表后,還要對(duì)鏈表遍歷,找出key相等的對(duì)象,替換它,并且返回舊的值。

      for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            if (e.key == NULL_KEY) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }


如果遍歷完了該位置的鏈表都沒(méi)有找到有key相等的,那么將當(dāng)前對(duì)象增加到鏈表里面去

  modCount++;
  addEntry(hash, (K) NULL_KEY, value, i);
  return null;


且看看addEntry方法

    void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);
    }

table[bucketIndex] = new Entry<K,V>(hash, key, value, e);新建一個(gè)Entry對(duì)象,并放在當(dāng)前位置的Entry鏈表的頭部,看看下面的 Entry構(gòu)造函數(shù)就知道了,注意紅色部分。

     Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n; 
            key = k;
            hash = h;
        }

 

 

如何擴(kuò)容? 

        當(dāng)put一個(gè)元素時(shí),如果達(dá)到了容量限制,HashMap就會(huì)擴(kuò)容,新的容量永遠(yuǎn)是原來(lái)的2倍。

上面的put方法里有這樣的一段:

if (size++ >= threshold)
            resize(2 * table.length);

這是擴(kuò)容判斷,要注意,并不是數(shù)據(jù)尺寸達(dá)到HashMap的最大容量時(shí)才擴(kuò)容,而是達(dá)到 threshold指定的值時(shí)就開(kāi)始擴(kuò)容,threshold=最大容量*加載因子。 看看resize方法

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable); 
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

重點(diǎn)看看紅色部分的 transfer方法

    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);  
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

tranfer方法將所有的元素重新哈希,因?yàn)樾碌娜萘孔兇?,所以每個(gè)元素的哈希值和位置都是不一樣的。



正確的使用HashMap

1:不要在并發(fā)場(chǎng)景中使用HashMap

           HashMap是線程不安全的,如果被多個(gè)線程共享的操作,將會(huì)引發(fā)不可預(yù)知的問(wèn)題,據(jù)sun的說(shuō)法,在擴(kuò)容時(shí),會(huì)引起鏈表的閉環(huán),在get元素時(shí),就會(huì)無(wú)限循環(huán),后果是cpu100%。

看看get方法的紅色部分

 public V get(Object key) {
    if (key == null)
        return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }
 


2:如果數(shù)據(jù)大小是固定的,那么最好給HashMap設(shè)定一個(gè)合理的容量值

        根據(jù)上面的分析,HashMap的初始默認(rèn)容量是16,默認(rèn)加載因子是0.75,也就是說(shuō),如果采用HashMap的默認(rèn)構(gòu)造函數(shù),當(dāng)增加數(shù)據(jù)時(shí),數(shù)據(jù)實(shí)際容量超過(guò)10*0.75=12時(shí),HashMap就擴(kuò)容,擴(kuò)容帶來(lái)一系列的運(yùn)算,新建一個(gè)是原來(lái)容量2倍的數(shù)組,對(duì)原有元素全部重新哈希,如果你的數(shù)據(jù)有幾千幾萬(wàn)個(gè),而用默認(rèn)的HashMap構(gòu)造函數(shù),那結(jié)果是非常悲劇的,因?yàn)镠ashMap不斷擴(kuò)容,不斷哈希,在使用HashMap的場(chǎng)景里,不會(huì)是多個(gè)線程共享一個(gè)HashMap,除非對(duì)HashMap包裝并同步,由此產(chǎn)生的內(nèi)存開(kāi)銷和cpu開(kāi)銷在某些情況下可能是致命的。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多