1. 程式人生 > 實用技巧 >HashMap、HashTable、ConcurrentHashMap使用和原理分析(以及記憶體優化)

HashMap、HashTable、ConcurrentHashMap使用和原理分析(以及記憶體優化)

HashMap、HashTable、ConcurrentHashMap使用和原理分析(以及記憶體優化)

雜湊碼
每個物件和基本型別都有的一個方法 hashCode() 可以獲取其hashCode
預設是 物件的地址經過hash演算法轉換的整數

String aa = "123";
String bb = "456";
String cc = "123";
String dd = aa;

aa、cc、dd的hashCode均為 48690 bb為51669
hash值相等 物件不一定一致
物件一致 hash值一定相等
因為他是一個雜湊演算法

equals和== 比較的是物件的地址 只有是同一個物件才返回true
只是String這種 重寫了equals方法 只要同一個物件或者值相等就返回true

而Hash相關的資料結構 如HashMap就是根據鍵物件的hash值來進行存放

HashMap


概念:
容量(capacity ): 預設16 一個桶中的容量
載入因子(load factor):預設0.75 即桶中的可利用大小
鏈地址法:(開雜湊方法):設散列表地址空間的位置從0~m-1,則通過對所有的Key用雜湊函式(hashCode())計算出存放的位置,具有相同地址的關鍵碼歸於一個子集合(桶),在同一個Bucket中的鍵值對物件採用連結串列的方式連結起來

HashMap非執行緒安全的 HashTable執行緒安全,但是他是鎖住整個整個table,ConcurrentHashMap也是執行緒安全的,鎖的是segment

為什麼HashMap執行緒不安全:
1 put()時,若兩個執行緒都put了同樣的hashcode的key,則值會被覆蓋
3 當A執行緒put()資料時都發現空間不夠,執行resize()時,而同時B執行緒也put()資料也發現空間不夠執行resize(),有可能在A執行緒rehash()生成新表時節點i->k,而B執行緒rehash()生成新表時又將節點k->j,導致生成了死迴圈(i.next=k;k.next=i;)當一旦進入這個連結串列,就會導致死迴圈。
解決:使用ConcurrentHashMap(見下方)

HashMap時間複雜度
若美好的狀態下沒有hash衝突 每個桶只有一個元素時間複雜度 O(1) ,最差是O(n) 紅黑樹則是O(logn)

HashMap原理/資料結構/HashMap怎麼解決衝突的:
根本:陣列 + 連結串列(jdk1.7)/陣列+連結串列+紅黑樹(jdk1.8)(當連結串列長度超過閾值(8)將連結串列轉為紅黑樹 時間複雜度降低為O(logn))
通過計算key的hash值,每個桶對應一個hash值,若發生了hash衝突,則將相同的元素作為連結串列的一個節點放到連結串列中

HashMap的hash演算法

int index =(n-1) & key的hashcode

2^n長度的HashMap更高效,因為hash值計算後相同的概率更小,所以衝突更少一些 因此預設長度為16,載入倍數也是2倍

HashMap怎麼擴容resize():若達到了閾值(容量*載入因子的大小),則會自動擴容2倍。jdk1.8在長度達到了8個時,還會升級為紅黑樹
擴容過程:基於新的容量重新執行reHash()演算法,得到這些元素在新table中的位置並進行復制處理,因此擴容是很耗時的


HashMap的問題
1 當地址不夠大時,HashMap會採用擴大當前空間兩倍的方式去增大空間,而且在此過程中也需要不斷的做hash運算,資料獲取時也是通過遍歷方式獲取資料
2 當hash衝突較多,則連結串列會過長,遍歷時間會過長


常見使用

建構函式
HashMap()
Constructs an empty HashMap with the default initial capacity (16) and the default load factor (0.75).
HashMap(int initialCapacity)
Constructs an empty HashMap with the specified initial capacity and the default load factor (0.75).
HashMap(int initialCapacity, float loadFactor)
Constructs an empty HashMap with the specified initial capacity and load factor.

1 size()
獲取map的大小

2 isEmpty()
返回map 是否為空

3 get(Object key)
根據key返回value

4 containsKey(Object key)
根據key是否存在返回true or false

5 put(K key,V value)
裝載 鍵值對
對於put進了相同的key而不同的values。則後面的values會覆蓋前面的values

6 putAll(Map< ? extends K,? extends V> m)
將形參的map的全部鍵值對傳遞給當前定義的map作為當前map的鍵值對

7 remove(Object key)
根據Key 移除 map中該鍵值對資料

8 clear()
清除map中所有資料

9 public Set keySet()
獲取map中所有的key(key的遍歷)
Set (集合中各元素唯一的)

for (String key : map.keySet()) {    
    System.out.println("key= "+ key);    
}  

或者

Set<String> keySet = map.keySet();
		System.out.println(keySet);

10 public Collection values()
返回map中所有的values (values的遍歷)

for (String v : map.values()) {    
    System.out.println("value= " + v);    
}   

11 遍歷map

for (Map.Entry<String, String> entry : map.entrySet()) {
	System.out.println("key:"+entry.getKey() + ", value:" + entry.getValue());
}

12 public boolean remove(Object key,Object value)
移除鍵值對返回布林值

13 replace(K key,V value)
替換鍵值對


Android中對HashMap的優化

1 通過SparseArray稀疏陣列的方式去節省記憶體空間
注意: key是為int 形式!!!
SparseArray: 由兩個陣列 分別存放key 和 values
並且 key的存放為int形式,減少了裝箱操作,採取了壓縮的方式來表示稀疏陣列的資料,並且通過二分查詢方式去裝載和讀取資料


使用:

SparseArray<valueType> array = new SparseArray<>();
  • 1

1 delete(int key) 或者 remove(int key)
移除key 對應的資料
2 get(int key)
獲取key對應的鍵值對
3 put(int key, E value)
新增鍵值對
4 append(int key, E value)
也是新增鍵值對,若新增的鍵是按順序遞增的,則更推薦使用該方式,因為可以提高效能。
5 size()
獲取SparseArray 大小

參考,官方文件:https://developer.android.com/reference/android/util/SparseArray.html


HashTable

基本原理和HashMap類似,執行緒安全的
key和value都不允許傳null:因為多執行緒情況下,不同調用時機,無法確認key根本不存在,key值沒有對映,還是值本身就是null的

只是用Synchronized對put()和get() 方法加鎖(synchronized)
鎖住的是整個table效率低


ConcurrentHashMap

基本原理與HashMap類似,但是是執行緒安全的
key和value都不允許傳null:因為多執行緒情況下,不同調用時機,無法確認key根本不存在,key值沒有對映,還是值本身就是null的

與HashTable鎖住整個物件不同,ConcurrentHashMap採用鎖分段技術,粒度更低,不是鎖整個table,有一個Segment<K,V> extends ReentrantLock的陣列(預設concurrencyLevel也是長度16,最大允許16個執行緒併發寫操作),只對每個Segment加鎖。不是同一個hash值的時候沒必要加鎖

segmentMask:length-1
segmentShift:32 - lg (length)

假設ConcurrentHashMap一共分為2^ n個段,每個段中有2^m個桶
定位段的演算法:

演算法:hashCode & (2^n-1)
程式碼:向右無符號右移segmentShift位,然後和segmentMask進行與操作

定位桶的演算法:

演算法:hashCode & (2^m-1)

為什麼讀取可以不加鎖:
用HashEntery物件的不變性來降低讀操作對加鎖的需求;
用Volatile變數協調讀寫執行緒間的記憶體可見性;
若讀時發生指令重排序現象,則加鎖重讀

put()方法:lock()、unlock()加鎖了
先定位段位置,再定位桶的位置。

resize()方法:
若達到了閾值(容量*載入因子的大小),則會自動擴容2倍。
擴容過程:基於新的容量重新執行reHash()演算法(對ConcurrentHashMap的某個段的重雜湊,因此ConcurrentHashMap的每個段所包含的桶位自然也就不盡相同),得到這些元素在新table中的位置並進行復制處理,因此擴容是很耗時的


WeakHashMap

若將軟/弱/虛引用物件當做key 所引用的物件作為value 即使回收了value 但是HashMap的大小還是不會變的,因為引用物件也是物件,引用物件本身並沒有被回收,因此得用weakHashMap,當key中的引用被gc掉之後,它會將相應的entry給移除掉,原因就是檢測ReferenceQueue是否為空 是否軟/弱/虛引用所引用的物件是否被回收
WeakHashMap是執行緒安全


LinkedHashMap

LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

是一個有序的HashMap,分為插入有序(預設)和訪問有序
通過recordAccess標誌位決定,true為訪問順序,預設為false

資料結構(怎麼保證有序):繼承HashMap,和HashMap差不多,也是陣列+連結串列(next指標),只是多了一個LinkedList雙向連結串列,每個節點都有一個before、after指標

Entry節點結構:

使用場景:有序的HashMap,如實現LRU (Least recently used, 最近最少使用)演算法

儲存實現:
put:

 //1 根據key的hashCode通過hash演算法: (n-1)&hashcode 得到在桶中的位置
int hash = hash(key.hashCode());           
//計算該鍵值對在陣列中的儲存位置(哪個桶)
int i = indexFor(hash, table.length);
//2 判斷該條鏈上是否存在hash值相同且key值相等的對映,若存在,則直接覆蓋 value
// 3 若不存在則建立新的Entry,並插入到LinkedHashMap中 
createEntry(hash, key, value, bucketIndex);
// 如果重寫了removeEldestEntry返回true,並且accessOrder為true則支援LRU演算法 預設返回false
Entry<K,V> eldest = header.after;  //雙向連結串列的第一個有效節點(header後的那個節點)為最近最少使用的節點
if (removeEldestEntry(eldest)) {  
    removeEntryForKey(eldest.key);  //如果有必要,則刪除掉該近期最少使用的節點
} else {  
    if (size >= threshold)  
        resize(2 * table.length);  //4 若超出了閾值,則擴容到原來的2倍  
}

利用LinkedHashMap實現LRU演算法
Glide中就是使用了LinkedHashMap實現LRU演算法:

public class LRU<K,V> extends LinkedHashMap<K, V> implements Map<K, V>{

@Override
    protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
        // 關鍵是該函式返回true 則會移除最近最少使用的Entry
        if (size() > 6) {
            return true;
        }
        return false;
    }
// 這裡的true也是必須的 將accessOrder置為true 使用訪問順序而非插入順序
LRU<String,String> lru = LRU<String,String>(16,0.75,true);
正常使用put和get即可