Java面試& HashMap實現原理分析

1. HashMap的數據結構

數據結構中有數組和鏈表來實現對數據的存儲,但這二者基本上是兩個極端。html

 數組

數組存儲區間是連續的,佔用內存嚴重,故空間複雜的很大。但數組的二分查找時間複雜度小,爲O(1);數組的特色是:尋址容易,插入和刪除困難;java

鏈表

鏈表存儲區間離散,佔用內存比較寬鬆,故空間複雜度很小,但時間複雜度很大,達O(N)。鏈表的特色是:尋址困難,插入和刪除容易。c++

哈希表

那麼咱們能不能綜合二者的特性,作出一種尋址容易,插入刪除也容易的數據結構?答案是確定的,這就是咱們要提起的哈希表。哈希表((Hash table)既知足了數據的查找方便,同時不佔用太多的內容空間,使用也十分方便。git

  哈希表有多種不一樣的實現方法,我接下來解釋的是最經常使用的一種方法—— 拉鍊法,咱們能夠理解爲「鏈表的數組」 ,如圖:程序員

 

 

  從上圖咱們能夠發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每一個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢。通常狀況是經過hash(key)%len得到,也就是元素的key的哈希值對數組長度取模獲得。好比上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。因此十二、2八、108以及140都存儲在數組下標爲12的位置。github

  HashMap其實也是一個線性的數組實現的,因此能夠理解爲其存儲數據的容器就是一個線性數組。這可能讓咱們很不解,一個線性的數組怎麼實現按鍵值對來存取數據呢?這裏HashMap有作一些處理。面試

  首先HashMap裏面實現一個靜態內部類Entry,其重要的屬性有 key , value, next,從屬性key,value咱們就能很明顯的看出來Entry就是HashMap鍵值對實現的一個基礎bean,咱們上面說到HashMap的基礎就是一個線性數組,這個數組就是Entry[],Map裏面的內容都保存在Entry[]裏面。算法

    /**
     * The table, resized as necessary. Length MUST Always be a power of two.
     */

    transient Entry[] table;編程

2. HashMap的存取實現

     既然是線性數組,爲何能隨機存取?這裏HashMap用了一個小算法,大體是這樣實現:數組

// 存儲時:
int hash = key.hashCode(); // 這個hashCode方法這裏不詳述,只要理解每一個key的hash是一個固定的int值
int index = hash % Entry[].length; Entry[index] = value; // 取值時:
int hash = key.hashCode(); int index = hash % Entry[].length; return Entry[index];

1)put

 
疑問:若是兩個key經過hash%Entry[].length獲得的index相同,會不會有覆蓋的危險?

  這裏HashMap裏面用到鏈式數據結構的一個概念。上面咱們提到過Entry類裏面有一個next屬性,做用是指向下一個Entry。打個比方, 第一個鍵值對A進來,經過計算其key的hash獲得的index=0,記作:Entry[0] = A。一會後又進來一個鍵值對B,經過計算其index也等於0,如今怎麼辦?HashMap會這樣作:B.next = A,Entry[0] = B,若是又進來C,index也等於0,那麼C.next = B,Entry[0] = C;這樣咱們發現index=0的地方其實存取了A,B,C三個鍵值對,他們經過next這個屬性連接在一塊兒。因此疑問不用擔憂。也就是說數組中存儲的是最後插入的元素。到這裏爲止,HashMap的大體實現,咱們應該已經清楚了。

public V put(K key, V value) { if (key == null) return putForNullKey(value); //null老是放在數組的第一個鏈表中
        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; //若是key在鏈表中已存在,則替換爲新value
            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; } 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); //參數e, 是Entry.next //若是size超過threshold,則擴充table大小。再散列
    if (size++ >= threshold) resize(2 * table.length); }

固然HashMap裏面也包含一些優化方面的實現,這裏也說一下。好比:Entry[]的長度必定後,隨着map裏面數據的愈來愈長,這樣同一個index的鏈就會很長,會不會影響性能?HashMap裏面設置一個因子,隨着map的size愈來愈大,Entry[]會以必定的規則加長長度。

2)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; }

3)null key的存取

null key老是存放在Entry[]數組的第一個元素。

 private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; } private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; }

4)肯定數組index:hashcode % table.length取模

HashMap存取時,都須要計算當前key應該對應Entry[]數組哪一個元素,即計算數組下標;算法以下:

/** * Returns index for hash code h. */
    static int indexFor(int h, int length) { return h & (length-1); }
按位取並,做用上至關於取模mod或者取餘%。
這意味着數組下標相同,並不表示hashCode相同。
 

5)table初始大小

public HashMap(int initialCapacity, float 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(); }

注意table初始大小並非構造函數中的initialCapacity!!

而是 >= initialCapacity的2的n次冪!!!!

————爲何這麼設計呢?——

3. 解決hash衝突的辦法

  1. 開放定址法(線性探測再散列,二次探測再散列,僞隨機探測再散列)
  2. 再哈希法
  3. 鏈地址法
  4. 創建一個公共溢出區

Java中hashmap的解決辦法就是採用的鏈地址法。

 

4. 再散列rehash過程

當哈希表的容量超過默認容量時,必須調整table的大小。當容量已經達到最大可能值時,那麼該方法就將容量調整到Integer.MAX_VALUE返回,這時,須要建立一張新表,將原表的映射到新表中。

/** * Rehashes the contents of this map into a new array with a * larger capacity. This method is called automatically when the * number of keys in this map reaches its threshold. * * If current capacity is MAXIMUM_CAPACITY, this method does not * resize the map, but sets threshold to Integer.MAX_VALUE. * This has the effect of preventing future calls. * * @param newCapacity the new capacity, MUST be a power of two; * must be greater than current capacity unless current * capacity is MAXIMUM_CAPACITY (in which case value * is irrelevant). */
    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); } /** * Transfers all entries from current table to newTable. */
    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; //從新計算index
                    int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } } }
  • 第二篇文章

 

1 數據結構:hash_map原理 
這是一節讓你深刻理解hash_map的介紹,若是你只是想囫圇吞棗,不想理解其原理,你卻是能夠略過這一節,但我仍是建議你看看,多瞭解一些沒有壞處。 

hash_map基於hash table(哈希表)。哈希表最大的優勢,就是把數據的存儲和查找消耗的時間大大下降,幾乎能夠當作是常數時間;而代價僅僅是消耗比較多的內存。然而在當前可利用內存愈來愈多的狀況下,用空間換時間的作法是值得的。另外,編碼比較容易也是它的特色之一。 

其基本原理是:使用一個下標範圍比較大的數組來存儲元素。能夠設計一個函數(哈希函數,也叫作散列函數),使得每一個元素的關鍵字都與一個函數值(即數組下標,hash值)相對應,因而用這個數組單元來存儲這個元素;也能夠簡單的理解爲,按照關鍵字爲每個元素「分類」,而後將這個元素存儲在相應「類」所對應的地方,稱爲桶。 

可是,不可以保證每一個元素的關鍵字與函數值是一一對應的,所以極有可能出現對於不一樣的元素,卻計算出了相同的函數值,這樣就產生了「衝突」,換句話說,就是把不一樣的元素分在了相同的「類」之中。 總的來講,「直接定址」與「解決衝突」是哈希表的兩大特色。 

hash_map,首先分配一大片內存,造成許多桶。是利用hash函數,對key進行映射到不一樣區域(桶)進行保存。其插入過程是: 

   1. 獲得key 
   2. 經過hash函數獲得hash值 
   3. 獲得桶號(通常都爲hash值對桶數求模) 
   4. 存放key和value在桶內。 

其取值過程是: 

   1. 獲得key 
   2. 經過hash函數獲得hash值 
   3. 獲得桶號(通常都爲hash值對桶數求模) 
   4. 比較桶的內部元素是否與key相等,若都不相等,則沒有找到。 
   5. 取出相等的記錄的value。 

hash_map中直接地址用hash函數生成,解決衝突,用比較函數解決。這裏能夠看出,若是每一個桶內部只有一個元素,那麼查找的時候只有一次比較。當許多桶內沒有值時,許多查詢就會更快了(指查不到的時候). 

因而可知,要實現哈希表, 和用戶相關的是:hash函數(hashcode)和比較函數(equals)。

     假定哈希函數將元素正確分佈在各桶之間,可爲基本操做(get 和 put)提供穩定的性能。迭代集合視圖所需的時間與 HashMap 實例的「容量」(桶的數量)及其大小(鍵-值映射關係數)的和成比例。因此,若是迭代性能很重要,則不要將初始容量設置得過高(或將加載因子設置得過低)。 HashMap 的實例有兩個參數影響其性能:初始容量 和加載因子容量 是哈希表中桶的數量,初始容量只是哈希表在建立時的容量。加載因子 是哈希表在其容量自動增長以前能夠達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,經過調用 rehash 方法將容量翻倍。 一般,默認加載因子 (.75) 在時間和空間成本上尋求一種折衷。加載因子太高雖然減小了空間開銷,但同時也增長了查詢成本(在大多數 HashMap 類的操做中,包括 get 和 put 操做,都反映了這一點)。在設置初始容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地下降 rehash 操做次數。若是初始容量大於最大條目數除以加載因子,則不會發生 rehash 操做。 若是不少映射關係要存儲在 HashMap 實例中,則相對於按需執行自動的 rehash 操做以增大表的容量來講,使用足夠大的初始容量建立它將使得映射關係能更有效地存儲。 注意,此實現不是同步的。若是多個線程同時訪問此映射,而其中至少一個線程從結構上修改了該映射,則它必須 保持外部同步。(結構上的修改是指添加或刪除一個或多個映射關係的操做;僅改變與實例已經包含的鍵關聯的值不是結構上的修改。)這通常經過對天然封裝該映射的對象進行同步操做來完成。若是不存在這樣的對象,則應該使用 Collections.synchronizedMap 方法來「包裝」該映射。最好在建立時完成這一操做,以防止對映射進行意外的不一樣步訪問,以下所示: Map m = Collections.synchronizedMap(new HashMap(...)); 

 

 

HashMap的工做原理是近年來常見的Java面試題。幾乎每一個Java程序員都知道HashMap,都知道哪裏要用HashMap,知道HashTable和HashMap之間的區別,那麼爲什麼這道面試題如此特殊呢?是由於這道題考察的深度很深。這題常常出如今高級或中高級面試中。投資銀行更喜歡問這個問題,甚至會要求你實現HashMap來考察你的編程能力。ConcurrentHashMap和其它同步集合的引入讓這道題變得更加複雜。讓咱們開始探索的旅程吧!

先來些簡單的問題

「你用過HashMap嗎?」 「什麼是HashMap?你爲何用到它?」

幾乎每一個人都會回答「是的」,而後回答HashMap的一些特性,譬如HashMap能夠接受null鍵值和值,而HashTable則不能;HashMap是非synchronized;HashMap很快;以及HashMap儲存的是鍵值對等等。這顯示出你已經用過HashMap,並且對它至關的熟悉。可是面試官來個急轉直下,今後刻開始問出一些刁鑽的問題,關於HashMap的更多基礎的細節。面試官可能會問出下面的問題:

「你知道HashMap的工做原理嗎?」 「你知道HashMap的get()方法的工做原理嗎?」

你也許會回答「我沒有詳查標準的Java API,你能夠看看Java源代碼或者Open JDK。」「我能夠用Google找到答案。」

但一些面試者可能能夠給出答案,「HashMap是基於hashing的原理,咱們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當咱們給put()方法傳遞鍵和值時,咱們先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象。」這裏關鍵點在於指出,HashMap是在bucket中儲存鍵對象和值對象,做爲Map.Entry。這一點有助於理解獲取對象的邏輯。若是你沒有意識到這一點,或者錯誤的認爲僅僅只在bucket中存儲值的話,你將不會回答如何從HashMap中獲取對象的邏輯。這個答案至關的正確,也顯示出面試者確實知道hashing以及HashMap的工做原理。可是這僅僅是故事的開始,當面試官加入一些Java程序員天天要碰到的實際場景的時候,錯誤的答案頻現。下個問題多是關於HashMap中的碰撞探測(collision detection)以及碰撞的解決方法:

「當兩個對象的hashcode相同會發生什麼?」 從這裏開始,真正的困惑開始了,一些面試者會回答由於hashcode相同,因此兩個對象是相等的,HashMap將會拋出異常,或者不會存儲它們。而後面試官可能會提醒他們有equals()和hashCode()兩個方法,並告訴他們兩個對象就算hashcode相同,可是它們可能並不相等。一些面試者可能就此放棄,而另一些還能繼續挺進,他們回答「由於hashcode相同,因此它們的bucket位置相同,‘碰撞’會發生。由於HashMap使用LinkedList存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在LinkedList中。」這個答案很是的合理,雖然有不少種處理碰撞的方法,這種方法是最簡單的,也正是HashMap的處理方法。但故事尚未完結,面試官會繼續問:

「若是兩個鍵的hashcode相同,你如何獲取值對象?」 面試者會回答:當咱們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,而後獲取值對象。面試官提醒他若是有兩個值對象儲存在同一個bucket,他給出答案:將會遍歷LinkedList直到找到值對象。面試官會問由於你並無值對象去比較,你是如何肯定肯定找到值對象的?除非面試者直到HashMap在LinkedList中存儲的是鍵值對,不然他們不可能回答出這一題。

其中一些記得這個重要知識點的面試者會說,找到bucket位置以後,會調用keys.equals()方法去找到LinkedList中正確的節點,最終找到要找的值對象。完美的答案!

許多狀況下,面試者會在這個環節中出錯,由於他們混淆了hashCode()和equals()方法。由於在此以前hashCode()屢屢出現,而equals()方法僅僅在獲取值對象的時候纔出現。一些優秀的開發者會指出使用不可變的、聲明做final的對象,而且採用合適的equals()和hashCode()方法的話,將會減小碰撞的發生,提升效率。不可變性使得可以緩存不一樣鍵的hashcode,這將提升整個獲取對象的速度,使用String,Interger這樣的wrapper類做爲鍵是很是好的選擇。

若是你認爲到這裏已經完結了,那麼聽到下面這個問題的時候,你會大吃一驚。「若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?」除非你真正知道HashMap的工做原理,不然你將回答不出這道題。默認的負載因子大小爲0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)同樣,將會建立原來HashMap大小的兩倍的bucket數組,來從新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫做rehashing,由於它調用hash方法找到新的bucket位置。

若是你可以回答這道問題,下面的問題來了:「你瞭解從新調整HashMap大小存在什麼問題嗎?」你可能回答不上來,這時面試官會提醒你當多線程的狀況下,可能產生條件競爭(race condition)。

當從新調整HashMap大小的時候,確實存在條件競爭,由於若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在LinkedList中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在LinkedList的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。這個時候,你能夠質問面試官,爲何這麼奇怪,要在多線程的環境下使用HashMap呢?:)

熱心的讀者貢獻了更多的關於HashMap的問題:

  1. 爲何String, Interger這樣的wrapper類適合做爲鍵? String, Interger這樣的wrapper類做爲HashMap的鍵是再適合不過了,並且String最爲經常使用。由於String是不可變的,也是final的,並且已經重寫了equals()和hashCode()方法了。其餘的wrapper類也有這個特色。不可變性是必要的,由於爲了要計算hashCode(),就要防止鍵值改變,若是鍵值在放入時和獲取時返回不一樣的hashcode的話,那麼就不能從HashMap中找到你想要的對象。不可變性還有其餘的優勢如線程安全。若是你能夠僅僅經過將某個field聲明成final就能保證hashCode是不變的,那麼請這麼作吧。由於獲取對象的時候要用到equals()和hashCode()方法,那麼鍵對象正確的重寫這兩個方法是很是重要的。若是兩個不相等的對象返回不一樣的hashcode的話,那麼碰撞的概率就會小些,這樣就能提升HashMap的性能。
  2. 咱們可使用自定義的對象做爲鍵嗎? 這是前一個問題的延伸。固然你可能使用任何對象做爲鍵,只要它遵照了equals()和hashCode()方法的定義規則,而且當對象插入到Map中以後將不會再改變了。若是這個自定義對象時不可變的,那麼它已經知足了做爲鍵的條件,由於當它建立以後就已經不能改變了。
  3. 咱們可使用CocurrentHashMap來代替HashTable嗎?這是另一個很熱門的面試題,由於ConcurrentHashMap愈來愈多人用了。咱們知道HashTable是synchronized的,可是ConcurrentHashMap同步性能更好,由於它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap固然能夠代替HashTable,可是HashTable提供更強的線程安全性。看看這篇博客查看Hashtable和ConcurrentHashMap的區別。

我我的很喜歡這個問題,由於這個問題的深度和廣度,也不直接的涉及到不一樣的概念。讓咱們再來看看這些問題設計哪些知識點:

  • hashing的概念
  • HashMap中解決碰撞的方法
  • equals()和hashCode()的應用,以及它們在HashMap中的重要性
  • 不可變對象的好處
  • HashMap多線程的條件競爭
  • 從新調整HashMap的大小

總結

HashMap的工做原理

HashMap基於hashing原理,咱們經過put()和get()方法儲存和獲取對象。當咱們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當獲取對象時,經過鍵對象的equals()方法找到正確的鍵值對,而後返回值對象。HashMap使用LinkedList來解決碰撞問題,當發生碰撞了,對象將會儲存在LinkedList的下一個節點中。 HashMap在每一個LinkedList節點中儲存鍵值對對象。

當兩個不一樣的鍵對象的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的LinkedList中。鍵對象的equals()方法用來找到鍵值對。

由於HashMap的好處很是多,我曾經在電子商務的應用中使用HashMap做爲緩存。由於金融領域很是多的運用Java,也出於性能的考慮,咱們會常常用到HashMap和ConcurrentHashMap。你能夠查看更多的關於HashMap和HashTable的文章。

 

  • 第三篇文章

hashmap本質數據加鏈表。根據key取得hash值,而後計算出數組下標,若是多個key對應到同一個下標,就用鏈表串起來,新插入的在前面。

看3段重要代碼摘要:

a:

public HashMap(int initialCapacity, float loadFactor) { int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); table = new Entry[capacity]; init(); } 
有3個關鍵參數: capacity:容量,就是數組大小 loadFactor:比例,用於擴容 threshold:=capacity*loadFactor 最多容納的Entry數,若是當前元素個數多於這個就要擴容(capacity擴大爲原來的2倍) b:
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; } 

根據key算hash值,再根據hash值取得數組下標,經過數組下標取出鏈表,遍歷鏈表用equals取出對應key的value。


c:   

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; } 

從數組(經過hash值)取得鏈表頭,而後經過equals比較key,若是相同,就覆蓋老的值,並返回老的值。(該key在hashmap中已存在)

不然新增一個entry,返回null。新增的元素爲鏈表頭,之前相同數組位置的掛在後面。

另外:modCount是爲了不讀取一批數據時,在循環讀取的過程當中發生了修改,就拋異常

  if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
         

下面看添加一個map元素

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); } 

新增後,若是發現size大於threshold了,就resize到原來的2倍

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

新建一個數組,並將原來數據轉移過去

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); } } } 

將原來數組中的鏈表一個個取出,而後遍歷鏈表中每一個元素,從新計算index並放入新數組。每一個處理的也放鏈表頭。

在取出原來數組鏈表後,將原來數組置空(爲了大數據量複製時更快的被垃圾回收?)

還有兩點注意:

static class Entry<K,V> implements Map.Entry<K,V>是hashmap的靜態內部類,iterator之類的是內部類,由於不是每一個元素都須要持有map的this指針。

HashMap把  transient Entry[] table;等變量置爲transient,而後override了readObject和writeObject,本身實現序列化。

 

  • ConcurrentHashMap:

在hashMap的基礎上,ConcurrentHashMap將數據分爲多個segment,默認16個(concurrency level),而後每次操做對一個segment加鎖,避免多線程鎖得概率,提升併發效率。

public V get(Object key) { int hash = hash(key.hashCode()); return segmentFor(hash).get(key, hash); } final Segment<K,V> segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; } 

in class Segment:

V get(Object key, int hash) { if (count != 0) { // read-volatile 
             HashEntry<K,V> e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); // recheck 
 } e = e.next; } } return null; } 
 /** * Reads value field of an entry under lock. Called if value * field ever appears to be null. This is possible only if a * compiler happens to reorder a HashEntry initialization with * its table assignment, which is legal under memory model * but is not known to ever occur. */ V readValueUnderLock(HashEntry<K,V> e) { lock(); try { return e.value; } finally { unlock(); } } 

注意,這裏在併發讀取時,除了key對應的value爲null以外,並無使用鎖,如何作到沒有問題的呢,有如下3點:
1.       HashEntry<K,V> getFirst(int hash) {
            HashEntry<K,V>[] tab = table;
            return tab[hash & (tab.length - 1)];
        }
這裏若是在讀取時數組大小(tab.length)發生變化,是會致使數據不對的,但transient volatile HashEntry<K,V>[] table;是volatile得,數組大小變化能馬上知道

2.    static final class HashEntry<K,V> {
        final K key;
        final int hash;
        volatile V value;
        final HashEntry<K,V> next;
這裏next是final的,就保證了一旦HashEntry取出來,整個鏈表就是正確的。

3.value是volatile的,保證了若是有put覆蓋,是能夠馬上看到的。

public V put(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, false); } V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) // ensure capacity 
 rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) e.value = value; } else { oldValue = null; ++modCount; tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // write-volatile 
 } return oldValue; } finally { unlock(); } } 

這裏除了加鎖操做,其餘和普通HashMap原理上無太大區別。

還有一點不理解的地方:

對於get和put/remove併發發生的時候,若是get的HashEntry<K,V> e = getFirst(hash);鏈表已經取出來了,這個時候put放入一個entry到鏈表頭,若是正好是須要取的key,是否仍是會取不出來?

remove時,會先去除須要remove的key,而後把remove的key前面的元素一個個接到鏈表頭,一樣也存在remove後,之前的head到了中間,也會漏掉讀取的元素。

                     ++modCount; HashEntry<K,V> newFirst = e.next; for (HashEntry<K,V> p = first; p != e; p = p.next) newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst; count = c; // write-volatile 
相關文章
相關標籤/搜索