散列表數據結構及實現原理

java中已知的基於散列表的數據結構有:hashmap,hashset,hashtable。LinkedHashMap,LinkedHashSet

散列表整合了數組和鏈表的特點

備註:以下集合的原理均爲jdk1.7下的
一.hashMap的結構如圖所示:

這裏寫圖片描述

對應源碼

  static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
        }

hashMap的採用的就是散列表。

HashMap是一個用於存儲Key-Value鍵值對的集合,每一個鍵值對也叫做Entry。這些個鍵值對(Entry)分散存儲在一個數組當中,這個數組就是HashMap的主幹。HashMap數組每一個元素的初始值都是Null。

對於HashMap,我們最常使用的是兩個方法:Get 和 Put。
1.Put方法的原理

@see 碼農翻身:http://chuansong.me/n/2045375251011

注意:
當index發生衝突時會使用頭插法的方式。有點類似於棧的思想,最後進去的最先被找到
2.Get方法的原理
@see 碼農翻身:http://chuansong.me/n/2045375251011

3.remove方法的原理

分析一下remove元素的時候做了幾步:

1、根據key的hash找到待刪除的鍵值對位於table的哪個位置上

2、記錄一個prev表示待刪除的Entry的前一個位置Entry,e可以認爲是當前位置

3、從table[i]開始遍歷鏈表,假如找到了匹配的Entry,要做一個判斷,這個Entry是不是table[i]:

(1)是的話,table[i]就直接是table[i]的下一個節點,後面的都不需要動

(2)不是的話,e的前一個Entry也就是prev,prev的next指向e的後一個節點,也就是next,這樣,e所代表的Entry就被踢出了,e的前後Entry就連起來了

參考自:http://www.cnblogs.com/xrq730/p/5052323.html
hashmap的算法

index = HashCode(Key) & (Length - 1)

HashMap擴容

默認大小爲16,負載因子0.75,擴容時將容量變爲原來的2倍以滿足容量爲2的冪。

HashMap默認大小爲什麼是16?
主要目的就是讓分佈更加均勻。Hash算法最終得到的index結果,完全取決於Key的Hashcode值的最後幾位。

hashmap爲何使用此算法及擴容方式請參考
@see 碼農翻身:http://chuansong.me/n/2045375251011

WeakHashMap

WeakHashMap,從名字可以看出它是某種 Map。它的特殊之處在於 WeakHashMap 裏的entry可能會被GC自動刪除,即使程序員沒有調用remove()或者clear()方法。

注意:將一對key, value放入到 WeakHashMap 裏並不能避免該key值被GC回收,除非在 WeakHashMap 之外還有對該key的強引用。

@see 淺談WeakHashMap http://www.importnew.com/23182.html

二、HashSet

HashSet的數據結構和hashmap基本一致,可以說HashSet就是由hashMap演變而來

類成員PRESENT

 // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

HashMap是鍵值對K-V模型的,HashSet則是普通的集合類型。這裏的PRESENT就是用來填充HashMap中的value的。
構造函數

HashSet提供了4種構造函數。

// 默認無參構造函數
    public HashSet() {
        map = new HashMap<>();
    }
    
    // 以現有集合構造
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
    
    // 帶參構造 initialCapacity: 初始大小  loadFactor:加載因子
    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    // 帶參構造 initialCapacity: 初始大小
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    
    // 僅用來構造LinkedHashSet使用
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

由此可以證明HashSet就是由hashMap演變而來

add 方法

 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

add方法很簡單,直接調用HashMap的put方法實現。value則是用PRESENT進行填充。

remove 方法

public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

remove方法也是同樣,調用HashMap的remove方法。

HashSet大多數的方法都是通過HashMap的方法來實現的。

獲取元素
hashSet沒有提供get()方法來獲取指定的元素,要想獲取元素只有通過遍歷的形式來獲取:

/** 
     * 返回對此set中元素進行迭代的迭代器。返回元素的順序並不是特定的。 
     *  
     * 底層實際調用底層HashMap的keySet來返回所有的key。 
     * 可見HashSet中的元素,只是存放在了底層HashMap的key上, 
     * value使用一個static final的Object對象標識。 
     * @return 對此set中元素進行迭代的Iterator。 
     */  
    public Iterator<E> iterator() {  
    return map.keySet().iterator();  
    }  

hashset保證元素的不重複

我們用table[index]表示已經找到的元素需要存儲的位置。先判斷該位置上有沒有元素(這個元素是HashMap內部定義的一個類Entity, 基本結構它包含三個類,key,value和指向下一個Entity的next),沒有的話就創建一個Entity<K,V>對象,在 table[index]位置上插入,這樣插入結束;如果有的話,通過鏈表的遍歷方式去逐個遍歷,看看有沒有已經存在的key,有的話用新的value替 換老的value;如果沒有,則在table[index]插入該Entity,把原來在table[index]位置上的Entity賦值給新的 Entity的next,這樣插入結束。(即頭插法)

總結:數組位置衝突時,先遍歷後插入
參考:Hashpmap的原理,HashMap怎樣保證key的唯一性 http://blog.csdn.net/o9109003234/article/details/44107811

hashCode方法 & equals方法
如果我們在使用HashSet來存儲自定義的對象時候,要記得重寫hashCode方法和equals方法。
我們將通過一個示例來說明重寫的必要性。

public class Test {

    String a;

    public Test(String a) {
        this.a = a;
    }
    
    public static void main(String[] args) {

        Set set = new HashSet();

        Test o1 = new Test("abc");
        Test o2 = new Test("abc");

        set.add(o1);
        set.add(o2);

        System.out.println(o1.equals(o2));
        System.out.println(set.size());

    }

上面程序輸出應該是:

false
2

爲什麼要重寫?
默認equals方法是通過比較引用是否來判斷2個對象是否一致的。o1和o2顯然是2個對象,他們是不相等的,他們各自的hashcode顯然是不同的,所以HashSet會把他們當做2個不同對象來處理。

重寫的思路就讓兩個對象的hashcode值相同如:

@Override
    public int hashCode() {

        return 123 * 31 + a.hashCode();
    }

    @Override
    public boolean equals(Object o) {

        Test test = (Test) o;
        return test.a.equals(this.a);
    }

參考:源碼分析之HashSet https://www.jianshu.com/p/43f92a4b0b6f

三、hashtable
hashtable的底層結構和hashmap基本一致:
不同點:

1、結構不同-繼承的父類不同

public class Hashtable extends Dictionary implements Map 
public class HashMap extends AbstractMap implements Map 

2.線程
Hashtable 線程安全很好理解,因爲它每個方法中都加入了Synchronize。
如:

public synchronized V put(K key, V value) {}
 public synchronized V get(Object key) {}

3、key和value是否允許null值
Hashtable中,key和value都不允許出現null值。
在HashMap中,null可以作爲鍵,這樣的鍵只有一個;可以有一個或多個鍵所對應的值爲null。

4.內部實現使用的數組初始化和擴容方式不同

  HashTable在不指定容量的情況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量一定要爲2的整數次冪,而HashMap則要求一定爲2的整數次冪。
  Hashtable擴容時,將容量變爲原來的2倍加1,而HashMap擴容時,將容量變爲原來的2倍。
  Hashtable和HashMap它們兩個內部實現方式的數組的初始大小和擴容的方式。HashTable中hash數組默認大小是11,增加的方式是 old*2+1。

5.哈希值的使用不同,HashTable直接使用對象的hashCode。而HashMap重新計算hash值並取最後那幾個。

四、LinkedHashMap

關於LinkedHashMap,先提兩點:

1、LinkedHashMap可以認爲是HashMap+LinkedList,即它既使用HashMap操作數據結構,又使用LinkedList維護插入元素的先後順序

2、LinkedHashMap的基本實現思想就是----多態。LinkedHashMap是HashMap的子類,自然LinkedHashMap也就繼承了HashMap中所有非private的方法。

其數據結構如下:
這裏寫圖片描述

其中前面四個是從HashMap.Entry中繼承過來的;後面兩個,是LinkedHashMap獨有的。不要搞錯了next和before、After,next是用於維護HashMap指定table位置上連接的Entry的順序的,before、After是用於維護Entry插入的先後順序的。

LinkedHashMap的插入就是HashMap+LinkedList的實現方式,以HashMap維護數據結構,以LinkList的方式維護數據插入順序。即LinkedHashMap在插入時保證順序主要是通過before和after節點的改變。

LinkedHashMap的刪除同時維護HashMap+LinkedList,HashMap維護數據結構,LinkList維護數據的原先順序,即LinkedHashMap在刪除時會打斷before和after節點,然後重組before和after所指引的位置來保護數據的順序。

LinkedHashMap進行遍歷時,迭代的是LinkedList這個雙向鏈表來進行迭代輸出,這樣就保證了元素的有序性

在LinkedHashMap的get方法中,通過HashMap中的getEntry方法獲取Entry對象。即用到的是HashMap,不會訪問到before和after節點。

利用LinkedHashMap實現LRU算法緩存

@see https://blog.csdn.net/sinat_34814635/article/details/79206821

五、LinkedHashSet
LinkedHashSet是對LinkedHashMap的簡單包裝,對LinkedHashSet的函數調用都會轉換成合適的LinkedHashMap方法,因此LinkedHashSet的實現和LinkedHashMap基本一致。其源碼如下:

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    ......
    // LinkedHashSet裏面有一個LinkedHashMap
    public LinkedHashSet(int initialCapacity, float loadFactor) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
    ......
    public boolean add(E e) {//簡單的方法轉換
        return map.put(e, PRESENT)==null;
    }
    ......
}

具體原理請參考
5)圖解集合6:LinkedHashMap http://www.cnblogs.com/xrq730/p/5052323.html
6)Java集合詳解5:深入理解LinkedHashMap和LRU緩存https://blog.csdn.net/a724888/article/details/80290276

六、ConcurrentHashMap

爲什麼引用ConcurrentHashMap?
1.高併發下的HashMap不安全

1.Hashmap在插入元素過多的時候需要進行Resize,Resize的條件是

HashMap.Size   >=  Capacity * LoadFactor。

2.Hashmap的Resize包含擴容和ReHash兩個步驟,
1.擴容
創建一個新的Entry空數組,長度是原數組的2倍。

注意:hashMap的擴容方法void resize(int newCapacity) 是在put(K key, V value)方法裏
2.ReHash
遍歷原Entry數組,把所有的Entry重新Hash到新數組。爲什麼要重新Hash呢?因爲長度擴大以後,Hash的規則也隨之改變。


假設一個HashMap已經到了Resize的臨界點。此時有兩個線程A和B,在同一時刻對HashMap進行Put操作,就有可能造成形成鏈表環:
當調用Get查找一個不存在的Key,而這個Key的Hash結果恰好等於3的時候,由於位置3帶有環形鏈表,所以程序將會進入死循環!

具體原理請參考:漫畫:高併發下的HashMap https://blog.csdn.net/minkeyto/article/details/78667944  

源碼分析待續?
Collections.synchronizedMap()待續?

解決方案:
1).HashTable或者Collections.synchronizedMap()
2)ConcurrentHashMap
2.HashTable是一個線程安全的類,它使用synchronized來鎖住整張Hash表來實現線程安全,即每次鎖住整張表讓線程獨佔,會產生性能問題

ConcurrentHashMap的數據結構

Segment
Segment是什麼呢?Segment本身就相當於一個HashMap對象。

同HashMap一樣,Segment包含一個HashEntry數組,數組中的每一個HashEntry既是一個鍵值對,也是一個鏈表的頭節點。

單一的Segment結構如下:

這裏寫圖片描述
像這樣的Segment對象,在ConcurrentHashMap集合中有多少個呢?有2的N次方個,共同保存在一個名爲segments的數組當中。

因此整個ConcurrentHashMap的結構如下:

這裏寫圖片描述

可以說,ConcurrentHashMap是一個二級哈希表。在一個總的哈希表下面,有若干個子哈希表。

Segment的作用:

Case1:不同Segment的併發寫入:不同Segment的寫入是可以併發執行的。
Case2:同一Segment的一寫一讀:同一Segment的寫和讀是可以併發執行的。
Case3:同一Segment的併發寫入:Segment的寫入是需要上鎖的,因此對同一Segment的併發寫入會被阻塞。

由此可見,ConcurrentHashMap當中每個Segment各自持有一把鎖。在保證線程安全的同時降低了鎖的粒度,讓併發操作效率更高。

實現原理

Get方法:

1.爲輸入的Key做Hash運算,得到hash值。

2.通過hash值,定位到對應的Segment對象

3.再次通過hash值,定位到Segment當中數組的具體位置。

Put方法:

1.爲輸入的Key做Hash運算,得到hash值。

2.通過hash值,定位到對應的Segment對象

3.獲取可重入鎖

4.再次通過hash值,定位到Segment當中數組的具體位置。

5.插入或覆蓋HashEntry對象。

6.釋放鎖。

Size方法:
Size方法的目的是統計ConcurrentHashMap的總元素數量, 自然需要把各個Segment內部的元素數量彙總起來。但是,如果在統計Segment元素數量的過程中,已統計過的Segment瞬間插入新的元素,這時候該怎麼辦呢?

ConcurrentHashMap的Size方法是一個嵌套循環,大體邏輯如下:

1.遍歷所有的Segment。

2.把Segment的元素數量累加起來。

3.把Segment的修改次數累加起來。

4.判斷所有Segment的總修改次數是否大於上一次的總修改次數。如果大於,說明統計過程中有修改,重新統計,嘗試次數+1;如果不是。說明沒有修改,統計結束。

5.如果嘗試次數超過閾值,則對每一個Segment加鎖,再重新統計。加了鎖之後,在統計過程中其他線程就無法進行put等操作了

6.再次判斷所有Segment的總修改次數是否大於上一次的總修改次數。由於已經加鎖,次數一定和上次相等。

7.釋放鎖,統計結束。

這種思想應用到了cas和樂觀鎖轉爲悲觀鎖的思想。

具體實現原理請參考:漫畫:什麼是ConcurrentHashMap?http://www.sohu.com/a/205451532_684445

參考資料有:
1)http://blog.csdn.net/lizhongkaide/article/details/50595719
2)碼農翻身:http://chuansong.me/n/2045375251011
3)HashTable和HashMap的區別詳解 http://blog.csdn.net/fujiakai/article/details/51585767
4)源碼分析之HashSet https://www.jianshu.com/p/43f92a4b0b6f
5)圖解集合6:LinkedHashMap http://www.cnblogs.com/xrq730/p/5052323.html
6)Java集合詳解5:深入理解LinkedHashMap和LRU緩存https://blog.csdn.net/a724888/article/details/80290276
7)Java集合框架源碼剖析:LinkedHashSet 和 LinkedHashMap http://www.cnblogs.com/CarpenterLee/p/5541111.html
8)漫畫:高併發下的HashMap https://blog.csdn.net/minkeyto/article/details/78667944
9)漫畫:什麼是ConcurrentHashMap?http://www.sohu.com/a/205451532_684445

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章