集合

2.1集合

這裏要討論這些常用的默認初始容量和擴容的原因是:

當底層實現涉及到擴容時,容器或重新分配一段更大的連續內存(如果是離散分配則不需要重新分配,離散分配都是插入新元素時動態分配內存),要將容器原來的數據全部複製到新的內存上,這無疑使效率大大降低。

加載因子的係數小於等於1,意指  即當 元素個數 超過 容量長度*加載因子的係數 時,進行擴容。

另外,擴容也是有默認的倍數的,不同的容器擴容情況不同。

 

List 元素是有序的、可重複

ArrayList、Vector默認初始容量爲10

Vector:線程安全,但速度慢

    底層數據結構是數組結構

    加載因子爲1:即當 元素個數 超過 容量長度 時,進行擴容

    擴容增量:原容量的 1倍

      如 Vector的容量爲10,一次擴容後是容量爲20

ArrayList:線程不安全,查詢速度快

    底層數據結構是數組結構

    擴容增量:原容量的 0.5倍+1

      如 ArrayList的容量爲10,一次擴容後是容量爲16

 

Set(集) 元素無序的、不可重複。

HashSet:線程不安全,存取速度快

     底層實現是一個HashMap(保存數據),實現Set接口

     默認初始容量爲16(爲何是16,見下方對HashMap的描述)

     加載因子爲0.75:即當 元素個數 超過 容量長度的0.75倍 時,進行擴容

     擴容增量:原容量的 1 倍

      如 HashSet的容量爲16,一次擴容後是容量爲32

 

Map是一個雙列集合

HashMap:默認初始容量爲16

     (爲何是16:16是2^4,可以提高查詢效率,另外,32=16<<1       -->至於詳細的原因可另行分析,或分析源代碼)

     加載因子爲0.75:即當 元素個數 超過 容量長度的0.75倍 時,進行擴容

     擴容增量:原容量的 1 倍

      如 HashSet的容量爲16,一次擴容後是容量爲32

 

Java Collection由兩套並行的接口組成,一套是Collection接口,一套是Map接口。如下圖

 

 

 

 

 

 

 

2.1.1 List

 

AbstractList

要求子類實現get(int)size()方法,AbstractList利用這兩個模板方法,實現出完整的只讀List

 

ArrayList

利用Object[]實現的List

 

AbstractSequentialList

利用ListIterator接口,實現get(index)set(index)remove(index)add(index, value)等隨機訪問的方法。

 

LinkedList

單向鏈表,下面是鏈表中每個節點的定義:

    privatestaticclass Node<E> {

        E item;

        Node<E> next;

        Node<E> prev;

    }

 

Vector

線程安全的List,採用synchronized(this)進行加鎖。內部採用Object[]實現。

 Stack

Vector進行的簡單封裝。

 CopyOnWriteArrayList

注意此類直接從Object派生。

線程安全。每個addset等修改操作,都會導致對內部數組進行全新複製。Iterator()函數返回的迭代器對象,包含了當前array的一個快照。因此,對CopyOnWriteArrayList對象的修改,不會影響已經生成的迭代器對象,只是迭代器對象看到的快照有可能是過時的。

 

 

2.1.2 Map

集合是編程中最常用的數據結構。而談到併發,幾乎總是離不開集合這類高級數據結構的支持。比如兩個線程需要同時訪問一箇中間臨界區 Queue),比如常會用緩存作爲外部文件的副本(HashMap)。這篇文章主要分析jdk1.5的3種併發集合類型 (concurrent,copyonright,queue)中的ConcurrentHashMap,讓我們從原理上細緻的瞭解它們,能夠讓我們在深 度項目開發中獲益非淺。

    在tiger之前,我們使用得最多的數據結構之一就是HashMap和Hashtable。大家都知道,HashMap中未進行同步考慮,而 Hashtable則使用了synchronized,帶來的直接影響就是可選擇,我們可以在單線程時使用HashMap提高效率,而多線程時用 Hashtable來保證安全。

    當我們享受着jdk帶來的便利時同樣承受它帶來的不幸惡果。通過分析Hashtable就知道,synchronized是針對整張Hash表的,即每次 鎖住整張表讓線程獨佔,安全的背後是巨大的浪費,慧眼獨具的Doug Lee立馬拿出瞭解決方案----ConcurrentHashMap。

ConcurrentHashMap和Hashtable主要區別就是圍繞着鎖的粒度以及如何鎖。如圖

 

左邊便是Hashtable的實現方式---鎖整個hash表;而右邊則是ConcurrentHashMap的實現方式---鎖桶(或段)。 ConcurrentHashMap將hash表分爲16個桶(默認值),諸如get,put,remove等常用操作只鎖當前需要用到的桶。試想,原來 只能一個線程進入,現在卻能同時16個寫線程進入(寫線程才需要鎖定,而讀線程幾乎不受限制,之後會提到),併發性的提升是顯而易見的。

    更令人驚訝的是ConcurrentHashMap的讀取併發,因爲在讀取的大多數時候都沒有用到鎖定,所以讀取操作幾乎是完全的併發操作,而寫操作鎖定 的粒度又非常細,比起之前又更加快速(這一點在桶更多時表現得更明顯些)。只有在求size等操作時才需要鎖定整個表。而在迭代 時,ConcurrentHashMap使用了不同於傳統集合的快速失敗迭代器(見之前的文章《JAVA API備忘---集合》)的另一種迭代方式,我們稱爲弱一致迭代器。在這種迭代方式中,當iterator被創建後集合再發生改變就不再是拋出 ConcurrentModificationException,取而代之的是在改變時new新的數據從而不影響原有的數據,iterator完成後再 將頭指針替換爲新的數據,這樣iterator線程可以使用原來老的數據,而寫線程也可以併發的完成改變,更重要的,這保證了多個線程併發執行的連續性和 擴展性,是性能提升的關鍵。

    接下來,讓我們看看ConcurrentHashMap中的幾個重要方法,心裏知道了實現機制後,使用起來就更加有底氣。

    ConcurrentHashMap中主要實體類就是三個:ConcurrentHashMap(整個Hash表),Segment(桶),HashEntry(節點),對應上面的圖可以看出之間的關係。

    get方法(請注意,這裏分析的方法都是針對桶的,因爲ConcurrentHashMap的最大改進就是將粒度細化到了桶上),首先判斷了當前桶的數據 個數是否爲0,爲0自然不可能get到什麼,只有返回null,這樣做避免了不必要的搜索,也用最小的代價避免出錯。然後得到頭節點(方法將在下面涉及) 之後就是根據hash和key逐個判斷是否是指定的值,如果是並且值非空就說明找到了,直接返回;程序非常簡單,但有一個令人困惑的地方,這句 return readValueUnderLock(e)到底是用來幹什麼的呢?研究它的代碼,在鎖定之後返回一個值。但這裏已經有一句V v = e.value得到了節點的值,這句return readValueUnderLock(e)是否多此一舉?事實上,這裏完全是爲了併發考慮的,這裏當v爲空時,可能是一個線程正在改變節點,而之前的 get操作都未進行鎖定,根據bernstein條件,讀後寫或寫後讀都會引起數據的不一致,所以這裏要對這個e重新上鎖再讀一遍,以保證得到的是正確 值,這裏不得不佩服Doug Lee思維的嚴密性。整個get操作只有很少的情況會鎖定,相對於之前的Hashtable,併發是不可避免的啊!

AbstractMap

抽象類,利用entrySet()模板方法實現了一些通用方法。

 

HashMap

哈希表

KeyValue都可以爲null

非線程安全。

 

LinkedHashMap

HashMap派生,整個哈希結構都是利用了HashMap來實現

每個插入的Value,都通過一個雙向鏈表連接起來。

可以指定鏈表的順序是按照插入時間排序、還是按照訪問時間排序。如果是按照訪問時間排序,每次訪問都要調整鏈表。

 

TreeMap

基於紅黑樹實現的Map

非線程安全。

 

ConcurrentHashTable

基於哈希表實現。

構造函數中可以指定併發程度,也就是預期有多少更新線程。

 

ConcurrentSkiptListMap

支持併發的跳躍表

 

IdentityHashMap

哈希表

內部採用==判斷key的相等性。

 

EnumMap

接收KeyEnum類型的Map

內部用數組來存儲Value,即Value == this.Vals[Key.ordinal],效率高。

 

WeakHashMap

每個KeyWeakReference管理。當Key對象符合弱引用的回收條件時,就被回收。

Size()方法返回的是還沒有被回收的Key對象的數量。

如果Key已經被回收,get方法放回null

利用哈希表實現。

實現上,每個<KeyValue>對應一個Entry,每個Entry都是WeakReference的派生類對象。該Entry對象也就是KeyWeakReference

 

 

2.3 set

 

AbstractSet

抽象基類,通過Iterator實現幾個通用方法。

 HashSet

內部包含了一個HashMap對象。

HashSet中添加一個元素X,相當於向HashMap對象添加一個<X,DummyObject>二元組。

非線程安全。

接收null元素。

 LinkedHashSet

內部包含了一個LinkedHashMap對象。

非線程安全。

接收null元素。

採用Iterator迭代的時候,根據元素添加的順序排序。

 TreeSet

內部包含了一個TreeMap對象。

TreeSet中添加一個元素X,相當於向TreeMap對象添加一個<X,DummyObject>二元組。

非線程安全。

不接受null元素。

 ConcurrentSkipListSet

內部包含了一個ConcurentSkipListMap

線程安全。

不接收null元素。

 CopyOnWriteArraySet

內部包含了一個CopyOnWriteArrayList做實際的數據存儲,因此這實際上是一個Array

2.4 queue

 

ArrayDeque

用環形數組實現的Deque,自動增長數組大小。

不能接受null元素。

非線程安全。

 AbstractQueue

抽象類。利用模板函數實現了幾個功能,比如addAll()

 ConcurrentLinkedQueue

線程安全。但這只是表示併發操作不會破壞內部結構,但是toArray()、迭代器、addAll()等操作不是原子的。

不能接受null元素。

用單向鏈表實現。

使用了非JDKsun.misc.Unsafe

 PriorityQueue

優先隊列,利用優先堆實現。

非線程安全。

加入的元素必須支持全排序。

不能接受null元素。

 DelayQueue

實現的時候用到了PriorityQueue

非線程安全。

支持Blocking操作。

可以用於連接超時自動移除、緩存超時自動移除等場景。

注意事項:假定DelayQueue中的元素類型爲T

1.        T.getDelay()應該返回超時值

2.        T必須可以全排序

3.        T最好是根據超時值進行全排序,並且全排序一旦排好,比較結果不應該隨着Delay值變化而變化。

 SynchronousQueue

線程安全。

不接收null元素。

特點是puttake函數必須同時執行,才能全部返回;任何一個單獨執行只會導致等待。

 PriorityBlockingQueue

帶有Blocking功能的優先隊列。

 LinkedBlockingQueue

帶有Blocking功能的Queue

用單向鏈表實現。

創建的時候可以指定容量,沒有指定容量就是Integer.MaxValue;容量在創建後不能修改。

已滿狀態執行put會導致等待。

爲空狀態下執行take會導致等待。

 ArrayBlockingQueue

帶有Blocking功能的Queue

Array實現。

創建的時候必須指定容量,並且創建後不能修改。

已滿狀態執行put會導致等待。

爲空狀態下執行take會導致等待。


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