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派生。
線程安全。每個add、set等修改操作,都會導致對內部數組進行全新複製。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
哈希表
Key和Value都可以爲null。
非線程安全。
LinkedHashMap
從HashMap派生,整個哈希結構都是利用了HashMap來實現
每個插入的Value,都通過一個雙向鏈表連接起來。
可以指定鏈表的順序是按照插入時間排序、還是按照訪問時間排序。如果是按照訪問時間排序,每次訪問都要調整鏈表。
TreeMap
基於紅黑樹實現的Map。
非線程安全。
ConcurrentHashTable
基於哈希表實現。
構造函數中可以指定併發程度,也就是預期有多少更新線程。
ConcurrentSkiptListMap
支持併發的跳躍表
IdentityHashMap
哈希表
內部採用==判斷key的相等性。
EnumMap
接收Key爲Enum類型的Map。
內部用數組來存儲Value,即Value == this.Vals[Key.ordinal],效率高。
WeakHashMap
每個Key用WeakReference管理。當Key對象符合弱引用的回收條件時,就被回收。
Size()方法返回的是還沒有被回收的Key對象的數量。
如果Key已經被回收,get方法放回null。
利用哈希表實現。
實現上,每個<Key,Value>對應一個Entry,每個Entry都是WeakReference的派生類對象。該Entry對象也就是Key的WeakReference。
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元素。
用單向鏈表實現。
使用了非JDK類sun.misc.Unsafe。
PriorityQueue
優先隊列,利用優先堆實現。
非線程安全。
加入的元素必須支持全排序。
不能接受null元素。
DelayQueue
實現的時候用到了PriorityQueue。
非線程安全。
支持Blocking操作。
可以用於連接超時自動移除、緩存超時自動移除等場景。
注意事項:假定DelayQueue中的元素類型爲T,
1. T.getDelay()應該返回超時值
2. T必須可以全排序
3. T最好是根據超時值進行全排序,並且全排序一旦排好,比較結果不應該隨着Delay值變化而變化。
SynchronousQueue
線程安全。
不接收null元素。
特點是put和take函數必須同時執行,才能全部返回;任何一個單獨執行只會導致等待。
PriorityBlockingQueue
帶有Blocking功能的優先隊列。
LinkedBlockingQueue
帶有Blocking功能的Queue
用單向鏈表實現。
創建的時候可以指定容量,沒有指定容量就是Integer.MaxValue;容量在創建後不能修改。
已滿狀態執行put會導致等待。
爲空狀態下執行take會導致等待。
ArrayBlockingQueue
帶有Blocking功能的Queue
用Array實現。
創建的時候必須指定容量,並且創建後不能修改。
已滿狀態執行put會導致等待。
爲空狀態下執行take會導致等待。