目錄html
轉載自:http://www.importnew.com/31278.htmljava
答不是有序的。node
TreeMap 和 LinkedHashMap。面試
1.TreeMap 是經過實現 SortMap 接口,可以把它保存的鍵值對根據 key 排序,基於紅黑樹,從而保證 TreeMap 中全部鍵值對處於有序狀態。
2.LinkedHashMap 則是經過插入排序(就是你 put 的時候的順序是什麼,取出來的時候就是什麼樣子)和訪問排序(改變排序把訪問過的放到底部)讓鍵值有序。算法
HashMap 是一個散列桶(數組和鏈表),它存儲的內容是鍵值對 key-value 映射
HashMap 採用了數組和鏈表的數據結構,能在查詢和修改方便繼承了數組的線性查找和鏈表的尋址修改
HashMap 是非 synchronized,因此 HashMap 很快
HashMap 能夠接受 null 鍵和值,而 Hashtable 則不能(緣由就是 equlas() 方法須要對象,由於 HashMap 是後出的 API 通過處理才能夠)數組
HashMap 是基於 hashing 的原理
咱們使用 put(key, value) 存儲對象到 HashMap 中,使用 get(key) 從 HashMap 中獲取對象。當咱們給 put() 方法傳遞鍵和值時,咱們先對鍵調用 hashCode() 方法,計算並返回的 hashCode 是用於找到 Map 數組的 bucket 位置來儲存 Node 對象。緩存
這裏關鍵點在於指出,HashMap 是在 bucket 中儲存鍵對象和值對象,做爲Map.Node 。安全
如下是 HashMap 初始化
簡化的模擬數據結構:數據結構
Node[] table = new Node[16]; // 散列桶初始化,table class Node { hash; //hash值 key; //鍵 value; //值 node next; //用於指向鏈表的下一層(產生衝突,用拉鍊法) }
如下是具體的 put 過程(JDK1.8)
對 Key 求 Hash 值,而後再計算下標
若是沒有碰撞,直接放入桶中(碰撞的意思是計算獲得的 Hash 值相同,須要放到同一個 bucket 中)
若是碰撞了,以鏈表的方式連接到後面
若是鏈表長度超過閥值(TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表
若是節點已經存在就替換舊值
若是桶滿了(容量16*加載因子0.75),就須要 resize(擴容2倍後重排)
如下是具體 get 過程
考慮特殊狀況:若是兩個鍵的 hashcode 相同,你如何獲取值對象?多線程
當咱們調用 get() 方法,HashMap 會使用鍵對象的 hashcode 找到 bucket 位置,找到 bucket 位置以後,會調用 keys.equals() 方法去找到鏈表中正確的節點,最終找到要找的值對象。
擾動函數能夠減小碰撞
原理是若是兩個不相等的對象返回不一樣的 hashcode 的話,那麼碰撞的概率就會小些。這就意味着存鏈表結構減少,這樣取值的話就不會頻繁調用 equal 方法,從而提升 HashMap 的性能(擾動即 Hash 方法內部的算法實現,目的是讓不一樣對象返回不一樣hashcode)。
使用不可變的、聲明做 final 對象,而且採用合適的 equals() 和 hashCode() 方法,將會減小碰撞的發生
不可變性使得可以緩存不一樣鍵的 hashcode,這將提升整個獲取對象的速度,使用 String、Integer 這樣的 wrapper 類做爲鍵是很是好的選擇。
由於 String 是 final,並且已經重寫了 equals() 和 hashCode() 方法了。不可變性是必要的,由於爲了要計算 hashCode(),就要防止鍵值改變,若是鍵值在放入時和獲取時返回不一樣的 hashcode 的話,那麼就不能從 HashMap 中找到你想要的對象。
咱們能夠看到,在 hashmap 中要找到某個元素,須要根據 key 的 hash 值來求得對應數組中的位置。如何計算這個位置就是 hash 算法。
前面說過,hashmap 的數據結構是數組和鏈表的結合,因此咱們固然但願這個 hashmap 裏面的元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個。那麼當咱們用 hash 算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表。 因此,咱們首先想到的就是把 hashcode 對數組長度取模運算。這樣一來,元素的分佈相對來講是比較均勻的。
可是「模」運算的消耗仍是比較大的,能不能找一種更快速、消耗更小的方式?咱們來看看 JDK1.8 源碼是怎麼作的(被樓主修飾了一下)
static final int hash(Object key) { if (key == null){ return 0; } int h; h = key.hashCode();返回散列值也就是hashcode // ^ :按位異或 // >>>:無符號右移,忽略符號位,空位都以0補齊 //其中n是數組的長度,即Map的數組部分初始化長度 return (n-1)&(h ^ (h >>> 16)); }
簡單來講就是:
高16 bit 不變,低16 bit 和高16 bit 作了一個異或(獲得的 hashcode 轉化爲32位二進制,前16位和後16位低16 bit和高16 bit作了一個異或)
(n·1) & hash = -> 獲得下標
之因此選擇紅黑樹是爲了解決二叉查找樹的缺陷:二叉查找樹在特殊狀況下會變成一條線性結構(這就跟原來使用鏈表結構同樣了,形成層次很深的問題),遍歷查找會很是慢。而紅黑樹在插入新數據後可能須要經過左旋、右旋、變色這些操做來保持平衡。引入紅黑樹就是爲了查找數據快,解決鏈表查詢深度的問題。咱們知道紅黑樹屬於平衡二叉樹,爲了保持「平衡」是須要付出代價的,可是該代價所損耗的資源要比遍歷線性鏈表要少。因此當長度大於8的時候,會使用紅黑樹;若是鏈表長度很短的話,根本不須要引入紅黑樹,引入反而會慢。
每一個節點非紅即黑
根節點老是黑色的
若是節點是紅色的,則它的子節點必須是黑色的(反之不必定)
每一個葉子節點都是黑色的空節點(NIL節點)
從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)
開放定址法
當衝突發生時,使用某種探查技術在散列表中造成一個探查(測)序列。沿此序列逐個單元地查找,直到找到給定的地址。按照造成探查序列的方法不一樣,可將開放定址法區分爲線性探查法、二次探查法、雙重散列法等。
下面給一個線性探查法的例子:
問題:已知一組關鍵字爲 (26,36,41,38,44,15,68,12,06,51),用除餘法構造散列函數,用線性探查法解決衝突構造這組關鍵字的散列表。
解答:爲了減小衝突,一般令裝填因子 α 由除餘法因子是13的散列函數計算出的上述關鍵字序列的散列地址爲 (0,10,2,12,5,2,3,12,6,12)。
前5個關鍵字插入時,其相應的地址均爲開放地址,故將它們直接插入 T[0]、T[10)、T[2]、T[12] 和 T[5] 中。
當插入第6個關鍵字15時,其散列地址2(即 h(15)=15%13=2)已被關鍵字 41(15和41互爲同義詞)佔用。故探查 h1=(2+1)%13=3,此地址開放,因此將 15 放入 T[3] 中。
當插入第7個關鍵字68時,其散列地址3已被非同義詞15先佔用,故將其插入到T[4]中。
當插入第8個關鍵字12時,散列地址12已被同義詞38佔用,故探查 hl=(12+1)%13=0,而 T[0] 亦被26佔用,再探查 h2=(12+2)%13=1,此地址開放,可將12插入其中。
相似地,第9個關鍵字06直接插入 T[6] 中;而最後一個關鍵字51插人時,因探查的地址 12,0,1,…,6 均非空,故51插入 T[7] 中。
HashMap 默認的負載因子大小爲0.75。也就是說,當一個 Map 填滿了75%的 bucket 時候,和其它集合類同樣(如 ArrayList 等),將會建立原來 HashMap 大小的兩倍的 bucket 數組來從新調整 Map 大小,並將原來的對象放入新的 bucket 數組中。這個過程叫做 rehashing。
由於它調用 hash 方法找到新的 bucket 位置。這個值只可能在兩個地方,一個是原下標的位置,另外一種是在下標爲 <原下標+原容量> 的位置。
從新調整 HashMap 大小的時候,確實存在條件競爭。
由於若是兩個線程都發現 HashMap 須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來。由於移動到新的 bucket 位置的時候,HashMap 並不會將元素放在鏈表的尾部,而是放在頭部。這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。多線程的環境下不使用 HashMap。
爲何多線程會致使死循環,它是怎麼發生的?
HashMap 的容量是有限的。當通過屢次元素插入,使得 HashMap 達到必定飽和度時,Key 映射位置發生衝突的概率會逐漸提升。這時候, HashMap 須要擴展它的長度,也就是進行Resize。
擴容:建立一個新的 Entry 空數組,長度是原數組的2倍
rehash:遍歷原 Entry 數組,把全部的 Entry 從新 Hash 到新數組
(這個過程比較燒腦,暫不做流程圖演示,有興趣去看看個人另外一篇博文「HashMap擴容全過程」)
數組 + 鏈表方式存儲
默認容量:11(質數爲宜)
put操做:首先進行索引計算 (key.hashCode() & 0x7FFFFFFF)% table.length;若在鏈表中找到了,則替換舊值,若未找到則繼續;當總元素個數超過 容量 * 加載因子 時,擴容爲原來 2 倍並從新散列;將新元素加到鏈表頭部
對修改 Hashtable 內部共享數據的方法添加了 synchronized,保證線程安全
默認容量不一樣,擴容不一樣
線程安全性:HashTable 安全
效率不一樣:HashTable 要慢,由於加鎖
咱們知道 Hashtable 是 synchronized 的,可是 ConcurrentHashMap 同步性能更好,由於它僅僅根據同步級別對 map 的一部分進行上鎖
ConcurrentHashMap 固然能夠代替 HashTable,可是 HashTable 提供更強的線程安全性
它們均可以用於多線程的環境,可是當 Hashtable 的大小增長到必定的時候,性能會急劇降低,由於迭代時須要被鎖定很長的時間。因爲 ConcurrentHashMap 引入了分割(segmentation),不論它變得多麼大,僅僅須要鎖定 Map 的某個部分,其它的線程不須要等到迭代完成才能訪問 Map。簡而言之,在迭代的過程當中,ConcurrentHashMap 僅僅鎖定 Map 的某個部分,而 Hashtable 則會鎖定整個 Map
CocurrentHashMap 是由 Segment 數組和 HashEntry 數組和鏈表組成
Segment 是基於重入鎖(ReentrantLock):一個數據段競爭鎖。每一個 HashEntry 一個鏈表結構的元素,利用 Hash 算法獲得索引肯定歸屬的數據段,也就是對應到在修改時須要競爭獲取的鎖。ConcurrentHashMap 支持 CurrencyLevel(Segment 數組數量)的線程併發。每當一個線程佔用鎖訪問一個 Segment 時,不會影響到其餘的 Segment
核心數據如 value,以及鏈表都是 volatile 修飾的,保證了獲取時的可見性
首先是經過 key 定位到 Segment,以後在對應的 Segment 中進行具體的 put 操做以下:
將當前 Segment 中的 table 經過 key 的 hashcode 定位到 HashEntry。
遍歷該 HashEntry,若是不爲空則判斷傳入的 key 和當前遍歷的 key 是否相等,相等則覆蓋舊的 value
不爲空則須要新建一個 HashEntry 並加入到 Segment 中,同時會先判斷是否須要擴容
最後會解除在 1 中所獲取當前 Segment 的鎖。
雖然 HashEntry 中的 value 是用 volatile 關鍵詞修飾的,可是並不能保證併發的原子性,因此 put 操做時仍然須要加鎖處理
首先第一步的時候會嘗試獲取鎖,若是獲取失敗確定就有其餘線程存在競爭,則利用 scanAndLockForPut() 自旋獲取鎖。
嘗試自旋獲取鎖
若是重試的次數達到了 MAX_SCAN_RETRIES 則改成阻塞鎖獲取,保證能獲取成功。最後解除當前 Segment 的鎖
CocurrentHashMap 拋棄了原有的 Segment 分段鎖,採用了 CAS + synchronized 來保證併發安全性。其中的 val next 都用了 volatile 修飾,保證了可見性。
最大特色是引入了 CAS
藉助 Unsafe 來實現 native code。CAS有3個操做數,內存值 V、舊的預期值 A、要修改的新值 B。當且僅當預期值 A 和內存值 V 相同時,將內存值V修改成 B,不然什麼都不作。Unsafe 藉助 CPU 指令 cmpxchg 來實現。
CAS 使用實例
對 sizeCtl 的控制都是用 CAS 來實現的:
-1 表明 table 正在初始化
N 表示有 -N-1 個線程正在進行擴容操做
若是 table 未初始化,表示table須要初始化的大小
若是 table 初始化完成,表示table的容量,默認是table大小的0.75倍,用這個公式算 0.75(n – (n >>> 2))
CAS 會出現的問題:ABA
解決:對變量增長一個版本號,每次修改,版本號加 1,比較的時候比較版本號。
put 過程
根據 key 計算出 hashcode
判斷是否須要進行初始化
經過 key 定位出的 Node,若是爲空表示當前位置能夠寫入數據,利用 CAS 嘗試寫入,失敗則自旋保證成功
若是當前位置的 hashcode == MOVED == -1,則須要進行擴容
若是都不知足,則利用 synchronized 鎖寫入數據
若是數量大於 TREEIFY_THRESHOLD 則要轉換爲紅黑樹
get 過程
根據計算出來的 hashcode 尋址,若是就在桶上那麼直接返回值
若是是紅黑樹那就按照樹的方式獲取值
就不知足那就按照鏈表的方式遍歷獲取值
ConcurrentHashMap 在 Java 8 中存在一個 bug 會進入死循環,緣由是遞歸建立 ConcurrentHashMap 對象,可是在 JDK 1.9 已經修復了。場景重現以下:
public class ConcurrentHashMapDemo{ private Map<Integer,Integer> cache =new ConcurrentHashMap<>(15); public static void main(String[]args){ ConcurrentHashMapDemo ch = new ConcurrentHashMapDemo(); System.out.println(ch.fibonaacci(80)); } public int fibonaacci(Integer i){ if(i==0||i ==1) { return i; } return cache.computeIfAbsent(i,(key) -> { System.out.println("fibonaacci : "+key); return fibonaacci(key -1)+fibonaacci(key - 2); }); } }