1. 程式人生 > >Java集合體系概述

Java集合體系概述

一、集合框架圖

Java的集合類主要由兩個介面派生而出:CollectionMap,Collection和Map是Java集合框架的根介面,這兩個介面又包含了一些子介面或實現類。

1:List集合:ArrayList LinkedList Vector 有序 可重複
2:Set集合:HashSet LinkedHashSet TreeSet 無序 不可重複
3:Map集合:HashMap LinkedHashMap WeakHashMap HashTable TreeMap 鍵值對 鍵唯一 值不唯一
4:執行緒安全的集合類:Vector HashTable ConcurrentHashMap Stack(繼承於Vector)
5:併發容器:ConcurrentHashMap  CopyOnWriteArrayList CopyOnWriteArraySet ArrayBlockingQueue BlockingQueue 

二、實現原理
1.ArrayList: 有序可重複,查詢速度快,增刪改速度慢。
ArrayList是List介面的可變陣列非同步實現,並允許包括null在內的所有元素。
底層使用陣列實現
該集合是可變長度陣列,陣列擴容時,會將老陣列中的元素重新拷貝一份到新的陣列中,每次陣列容量增長大約是其容量的1.5倍,這種操作的代價很高。
採用了Fail-Fast機制,面對併發的修改時,迭代器很快就會完全失敗,而不是冒著在將來某個不確定時間發生任意不確定行為的風險
remove方法會讓下標到陣列末尾的元素向前移動一個單位,並把最後一位的值置空,方便GC
2.LinkedList:查詢速度慢,增刪改速度快。
LinkedList是List介面的雙向連結串列非同步實現,並允許包括null在內的所有元素。
底層的資料結構是基於雙向連結串列的,該資料結構我們稱為節點
雙向連結串列節點對應的類Node的例項,Node中包含成員變數:prev,next,item。其中,prev是該節點的上一個節點,next是該節點的下一個節點,item是該節點所包含的值。
它的查詢是分兩半查詢,先判斷index是在連結串列的哪一半,然後再去對應區域查詢,這樣最多隻要遍歷連結串列的一半節點即可找到
3.HashMap:
HashMap是基於雜湊表的Map介面的非同步實現,允許使用null值和null鍵,但不保證對映的順序。
底層使用陣列實現,陣列中每一項是個單向連結串列,即陣列和連結串列的結合體;當連結串列長度大於一定閾值時,連結串列轉換為紅黑樹,這樣減少連結串列查詢時間。
HashMap在底層將key-value當成一個整體進行處理,這個整體就是一個Node物件。HashMap底層採用一個Node[]陣列來儲存所有的key-value對,當需要儲存一個Node物件時,會根據key的hash演算法來決定其在陣列中的儲存位置,在根據equals方法決定其在該陣列位置上的連結串列中的儲存位置;當需要取出一個Node時,也會根據key的hash演算法找到其在陣列中的儲存位置,再根據equals方法從該位置上的連結串列中取出該Node。
HashMap進行陣列擴容需要重新計算擴容後每個元素在陣列中的位置,很耗效能
採用了Fail-Fast機制,通過一個modCount值記錄修改次數,對HashMap內容的修改都將增加這個值。迭代器初始化過程中會將這個值賦給迭代器的expectedModCount,在迭代過程中,判斷modCount跟expectedModCount是否相等,如果不相等就表示已經有其他執行緒修改了Map,馬上丟擲異常
4.Hashtable:
Hashtable是基於雜湊表的Map介面的同步實現,不允許使用null值和null鍵
底層使用陣列實現,陣列中每一項是個單鏈表,即陣列和連結串列的結合體
Hashtable在底層將key-value當成一個整體進行處理,這個整體就是一個Entry物件。Hashtable底層採用一個Entry[]陣列來儲存所有的key-value對,當需要儲存一個Entry物件時,會根據key的hash演算法來決定其在陣列中的儲存位置,在根據equals方法決定其在該陣列位置上的連結串列中的儲存位置;當需要取出一個Entry時,也會根據key的hash演算法找到其在陣列中的儲存位置,再根據equals方法從該位置上的連結串列中取出該Entry。
synchronized是針對整張Hash表的,即每次鎖住整張表讓執行緒獨佔
5.ConcurrentHashMap:
ConcurrentHashMap允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術。
它使用了多個鎖來控制對hash表的不同段進行的修改,每個段其實就是一個小的hashtable,它們有自己的鎖。只要多個併發發生在不同的段上,它們就可以併發進行。
ConcurrentHashMap在底層將key-value當成一個整體進行處理,這個整體就是一個Entry物件。Hashtable底層採用一個Entry[]陣列來儲存所有的key-value對,當需要儲存一個Entry物件時,會根據key的hash演算法來決定其在陣列中的儲存位置,在根據equals方法決定其在該陣列位置上的連結串列中的儲存位置;當需要取出一個Entry時,也會根據key的hash演算法找到其在陣列中的儲存位置,再根據equals方法從該位置上的連結串列中取出該Entry。
與HashMap不同的是,ConcurrentHashMap使用多個子Hash表,也就是段(Segment)
ConcurrentHashMap完全允許多個讀操作併發進行,讀操作並不需要加鎖。如果使用傳統的技術,如HashMap中的實現,如果允許可以在hash鏈的中間新增或刪除元素,讀操作不加鎖將得到不一致的資料。ConcurrentHashMap實現技術是保證HashEntry幾乎是不可變的。
6.HashSet: 無序不可重複
 HashSet實現Set介面,由雜湊表(實際上是一個HashMap例項)支援。它不保證set 的迭代順序;特別是它不保證該順序恆久不變。此類允許使用null元素。
 基於HashMap實現,API也是對HashMap的行為進行了封裝,可參考HashMap
7.LinkedHashMap:有序的
LinkedHashMap繼承於HashMap,底層使用雜湊表和雙向連結串列來儲存所有元素,並且它是非同步,允許使用null值和null鍵。
基本操作與父類HashMap相似,通過重寫HashMap相關方法,重新定義了陣列中儲存的元素Entry,來實現自己的連結列表特性。該Entry除了儲存當前物件的引用外,還儲存了其上一個元素before和下一個元素after的引用,從而構成了雙向連結列表。
LinkedHashMap 繼承自 HashMap,具有高效性,同時在 HashMap 的基礎上,又在內部增加了一個連結串列,用以存放元素的順序。LinkedHashMap 是基於元素進入集合的順序或者被訪問的先後順序排序,是有序的,按照存入的順序排序,而 TreeMap 則根據元素的 Key 進行排序。
8.LinkedHashSet:有序的
 LinkedHashSet是具有可預知迭代順序的Set介面的雜湊表和連結列表實現。此實現與HashSet的不同之處在於,後者維護著一個運行於所有條目的雙重連結列表。此連結列表定義了迭代順序,該迭代順序可為插入順序或是訪問順序。
注意,此實現不是同步的。如果多個執行緒同時訪問連結的雜湊Set,而其中至少一個執行緒修改了該Set,則它必須保持外部同步。
9.TreeMap:
TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的。
TreeMap 繼承於AbstractMap,所以它是一個Map,即一個key-value集合。
TreeMap 實現了NavigableMap介面,意味著它支援一系列的導航方法。比如返回有序的key集合。
TreeMap 實現了Cloneable介面,意味著它能被克隆。
TreeMap 實現了java.io.Serializable介面,意味著它支援序列化。
TreeMap基於紅黑樹(Red-Black tree)實現。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的 Comparator 進行排序,具體取決於使用的構造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
另外,TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
TreeMap實現SortMap介面,能夠把它儲存的記錄根據鍵排序。 
預設是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator 遍歷TreeMap時,得到的記錄是排過序的。

併發包中適用於讀多寫少的併發場景的工具類有:ConcurrentHashMap  CopyOnWriteArrayList CopyOnWriteArraySet

CopyOnWriteArrayList:該容器採用了寫時複製(copy-on-write)的優化策略,即當需要向容器中新增元素時,不直接往當前容器中新增,而是複製一個新的容器,
再往新的容器中新增修改元素,操作完成之後再將原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,
而不需要加鎖,因為當前容器不會新增任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
CopyOnWrite併發容器用於讀多寫少的併發場景。比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜尋網站,使用者在這個網站的搜尋框中,輸入關鍵字搜尋內容,但是某些關鍵字不允許被搜尋。這些不能被搜尋的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜尋時,會檢查當前關鍵字在不在黑名單當中

程式碼很簡單,但是使用CopyOnWriteMap需要注意兩件事情:

  1. 減少擴容開銷。根據實際需要,初始化CopyOnWriteMap的大小,避免寫時CopyOnWriteMap擴容的開銷。

  2. 使用批量新增。因為每次新增,容器每次都會進行復制,所以減少新增次數,可以減少容器的複製次數。如使用上面程式碼裡的addBlackList方法。

CopyOnWrite容器有很多優點,但是同時也存在兩個問題,即記憶體佔用問題和資料一致性問題。所以在開發的時候需要注意一下。

  記憶體佔用問題。因為CopyOnWrite的寫時複製機制,所以在進行寫操作的時候,記憶體裡會同時駐紮兩個物件的記憶體,舊的物件和新寫入的物件(注意:在複製的時候只是複製容器裡的引用,只是在寫的時候會建立新物件新增到新容器裡,而舊容器的物件還在使用,所以有兩份物件記憶體)。如果這些物件佔用的記憶體比較大,比如說200M左右,那麼再寫入100M資料進去,記憶體就會佔用300M,那麼這個時候很有可能造成頻繁的Yong GC和Full GC。之前我們系統中使用了一個服務由於每晚使用CopyOnWrite機制更新大物件,造成了每晚15秒的Full GC,應用響應時間也隨之變長。

  針對記憶體佔用問題,可以通過壓縮容器中的元素的方法來減少大物件的記憶體消耗,比如,如果元素全是10進位制的數字,可以考慮把它壓縮成36進位制或64進位制。或者不使用CopyOnWrite容器,而使用其他的併發容器,如ConcurrentHashMap。

  資料一致性問題。CopyOnWrite容器只能保證資料的最終一致性,不能保證資料的實時一致性。所以如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器。

參考文獻: