應用開發中集合就像水一樣離不開,這裡對ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、Hashtable、ConcurrentHashMap和TreeMap的關係做一點對比,雖然這些型別都是很常用的,基本上也都大概瞭解,但一直沒有深入理解,現在就做一些梳理,以便更準確的應用。
1. 資料儲存結構的區別
首先,大家都知道List、Set、Map這3個大類的區別,當然是很明顯的,就不再重複了。
List裡面最常用的ArrayList就是線性連續列表,底層用的陣列,預設或提供一個容量(capacity)引數,初始化時就建立一個capacity大小的陣列,當長度超過capacity*載入因子(loadFactory)並且小於最大長度時,就會建立一個容量翻倍的新陣列,然後把原陣列儲存的資料拷貝到新陣列,其實很多容器的實現都是基於類似的方式的。LinkedList名字就很明顯,是連結串列實現的列表,具體是一個雙向連結串列,其他沒有什麼特別的。
Map是特別重要的儲存容器,鍵值對結構經常是特別有用,常用型別也比較多:HashMap、Hashtable、ConcurrentHashMap和TreeMap,各自都有很大區別。從底層資料結構來看,主要就是分兩大類,一類是二叉樹結構的TreeMap,另一類是雜湊表結構,但其中的ConcurrentHashMap其實又有一些不一樣,等會兒再來說明,不過有人會說明明有一個LinkedHashMap,看名字就知道是鏈式結構嘛,其實看名字也會發現,主體還是HashMap,其實也確實是繼承自HashMap的,仔細閱讀原始碼,會發現這個鏈式結構是在底層雜湊儲存的基礎上再次封裝實現的,LinkedHashMap.Entry繼承自HashMap.Entry,擴充套件了雙向連結欄位,並在原操作(如get、put、remove等等)基礎上再進行連結操作,所以也會比HashMap稍微慢一點點(並不精準的測試了一下,各操作大約平均慢12%左右,僅作參考)。TreeMap是典型的二叉樹實現,按照預設(自然)或指定(Comparator)順序排列,例如查詢就是二分查詢,從根節點開始,通過比較確定一個子數,如此遍歷。雜湊結構就是按雜湊值排序,最底層其實都是陣列+連結串列,用陣列來存放對應雜湊值(鍵的雜湊值)的Entry(鍵值對元素,帶有連結指標next)連結串列,如下圖。
capacity取預設或設定值,當put一個Entry時,先計算key的hash(只有Hashtable是採用原始的hashCode,其他都對hashCode做了再處理),然後對capacity取模作為陣列index,取出table對應元素,遍歷連結串列,如果沒有相等(不相等的定義是hashCode不相等或者equals返回false)物件,就存放在連結串列末尾。如果元素總數大於capacity*loadFactory,同樣也要擴大table,一般是加倍,然後把原有全部Entry的hashCode重新取模重新存放(ConcurrentHashMap較特別,不是用原有Entry物件,而是直接克隆新的Entry,這與快速執行緒安全實現相關)。可能會覺得比較奇怪,這樣的結構不應該只能存放capacity個Entry,如果所有元素雜湊值取模是一樣的,豈不全部集中在table的一個index上?事實的確是這樣的,可以自己寫個程式重寫hashCode驗證一下,但是,在實際應用中,雜湊值是一種相對隨機的數值,因此在這個結構中,Entry實際是相對平均的分佈在個index中。又有人可能覺得奇怪,這樣的結構容量可不止capacity了,為啥還要擴充套件?這個我想主要是為了演算法效率,連結串列越短,遍歷也越快。當然這只是雜湊結構的基礎形式,具體來說ConcurrentHashMap相對就更特別一點,在此基礎上引入分段的概念,這也是為了加互斥鎖能更細粒度,加大執行緒併發。
最後再來看看Set,以前一直覺得Set一個節點只有一個資料,理應比Map結構簡單,仔細一看才發現,JDK的Set實現是基於Map的封裝,也就是說,底層是通過Map儲存資料的,只是再提供特定的訪問介面遮蔽Map結構,具體儲存鍵值對只用到了key,value採用的是一個常量。HashSet就是持有了一個HashMap物件,LinkedHashSet類似LinkedHashMap,繼承自HashSet,並且初始化時將HashMap建立成了LinkedHashMap例項,其他的具體是現就比較簡單,不需要細說了,所以,也可以預見,Set雖然看起來比Map簡單,但速度是基本一致的,實測也確實如此,在ns級差別也可忽略不計。
2. 執行緒安全(thread-safe)
都知道HashMap和TreeMap是不安全的,Hashtable和ConcurrentHashMap是執行緒安全的,ConcurrentHashMap比Hashtable速度快。具體來看保證併發訪問安全的實現,Hashtable和ConcurrentHashMap確大有不同。
Javadoc中明確說明Hashtable is synchronized,可以從Hashtable的原始碼中看到,讀寫相關方法(如:get、put、remove、clone等等),全部採用synchronized關鍵字進行方法同步,這樣效率肯定是最低的。在文件中也提到,Hashtable是較早的實現,現在應當使用ConcurrentHashMap替代。如果使用Collections中一系列轉同步集合的方法,也是使用加 synchronized 關鍵字,不過是包裝的塊同步不是方法同步。
ConcurrentHashMap文件中寫到“This class obeys the same functional specification as java.util.Hashtable”,可見ConcurrentHashMap定位就是用來替代Hashtable的。可以看到ConcurrentHashMap屬於JDK1.5加入的java.util.concurrent包,就是為了解決併發程式設計的問題,原始碼中也可以看到ConcurrentHashMap用到了新提供的ReentrantLock,文件介紹ReentrantLock是一個可重入的互斥鎖的實現,類似於synchronized的語義,不過更強大,最重要的是 ConcurrentHashMap 引入了分段的結構,每次加鎖都可以只對一個片段加鎖,不影響其他段,而且Entry採用了final欄位,因此讀取也可以不用加鎖,這樣減少鎖的情境而且更加細粒度,大大提高了併發效能。
3.資料操作方式的區別
這裡必須要提到 ConcurrentHashMap這個最特別的型別,其他型別資料操作都是最普通的方式,唯獨 ConcurrentHashMap採用了Unsafe類的直接操作記憶體的方式,應該也是為了操作原子化,不知道會不會也有效率提升,沒有測試過,大家可以試試,不過 Unsafe並沒有公開,平時使用的確也不安全,還是看看就行了。