類集框架面試
List
java.util.Vector
-
Vector對幾乎所有方法都加了鎖,且大部分都是synchronized方法,包括get方法,鎖的粒度較粗,性能較差,在jdk1.0版本就已經發布,很老舊的類,目前Java已經不推薦使用。
-
Vector初始默認容量爲10,默認每次擴容爲原來的一倍,可手動指定擴容大小
-
數據存放在一個Object類型的數組中
-
線程安全
java.util.ArrayList
-
線程不安全
-
默認初始容量爲10,擴容每次增加原來的一半
-
oldCapacity + (oldCapacity >> 1)
-
-
數據存儲在Object類型的名爲elementData的數組中,除此之外還有名爲EMPTY_ELEMENTDATA的Object空數組用於空實例共享,名爲DEFAULTCAPACITY_EMPTY_ELEMENTDATA的Object類型空數組用於默認大小的空實例的共享空數組實例。我們將其與EMPTY_ELEMENTDATA區分開來,以知道在添加第一個元素時要膨脹多少。
-
默認數組最大長度爲Integer.MAX_VALUE - 8,這是因爲有些虛擬機需要在數組中保留一些頭字,如果嘗試分配較大的數組可能會導致OutOfMemoryError
-
每次擴容時會傳遞一個名爲minCapacity的參數,該參數爲當前長度+1,如果這個值是負數(超出int最大值會變爲負數,補碼+1)會拋出OOM,如果新空間大於最大長度,會調用hugeCapacity方法,該方法會判斷當前容量+1是否已經超過最大長度,如果超過會返回Integer.MAX_VALUE,如果未超過會返回最大長度,也就是說並不一定在任何情況下擴容都是原來的1.5倍,有以下兩種情況:
1. 計算新容量後發現新容量比當前長度+1還要小(溢出等因素),新容量會改成當前長度+1 2. 計算新容量後發現比MAX_ARRAY_SIZE還要大,新容量會根據情況改爲Integer.MAX_VALUE或者MAX_ARRAY_SIZE或者拋出OOM。
java.util.LinkedList
-
線程不安全
-
雙向鏈表,存儲結構爲一個私有的靜態內部類Node,結構如下
-
private static class Node<E> { E item; Node<E> next; Node<E> prev; Node(Node<E> prev, E element, Node<E> next) { this.item = element; this.next = next; this.prev = prev; } }
-
-
jdk1.6後加入了descendingIterator方法返回迭代器用於逆向遍歷
-
添加元素時int類型的size會++,但是源碼中並沒有考慮溢出的情況,沒有判斷也沒有拋出任何異常
java.util.concurrent.CopyOnWriteArrayList
- 線程安全(廢話,JUC包下的)
- 使用ReentrantLock加鎖
- 使用volatile修飾的Object類型的數組
- 默認創建空數組
- 讀不加鎖寫加鎖,且都是在finally中釋放鎖
- 執行寫操作時會調用Arrays.copyOf方法創建新數組,並在新數組中寫,寫後將原引用替換爲新數組,所有操作都在加鎖情況下進行,這樣確定不會頻繁觸發gc?
Map
java.util.HashMap
-
線程不安全
-
成員變量
-
DEFAULT_INITIAL_CAPACITY = 1 << 4; /** *初始容量爲16,必須爲2的整數冪,原因後面會說 */
-
MAXIMUM_CAPACITY = 1 << 30; /** *最大容量2的30次方 */
-
DEFAULT_LOAD_FACTOR = 0.75f; /** *默認負載因子 */
-
TREEIFY_THRESHOLD = 8; /** *樹化閾值,該值必須大於2,且至少應爲8,纔可以符合轉化回鏈表的一個假設 */
-
UNTREEIFY_THRESHOLD = 6; /** *轉回鏈表的閾值,應小於TREEIFY_THRESHOLD且不超過6 */
-
MIN_TREEIFY_CAPACITY = 64; /** *最小樹化閾值,只有當哈希表容量大於該值,才允許進行樹化,否則會進行一次擴容 */
-
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; } /** *hash表基本結構 */
-
Node<K,V>[] table; /** *存放每一根鏈表的數組,在第一次使用時初始化 */
-
Set<Map.Entry<K,V>> entrySet; /** *鍵值對集合,沒什麼好說的 */
-
table數組會在第一次使用時初始化,第一次使用時會調用resize方法,在resize方法中會進行創建數組等操作
-
之所以使用8這個數字作爲樹化閾值,是因爲當使用分佈良好的哈希算法時,很少會樹化,也就是說每一個桶中鏈表的長度很少可以達到8個。在理想情況下,使用隨機hash算法,桶中節點的的頻率(數量)服從泊松分佈,當負載因子爲0.75時,平均參數約爲0.5,忽略方差,每一個桶中的節點數量的預期出現次數爲 (exp(-0.5) * pow(0.5, k) / factorial(k)),即:
-
出現次數 概率 1: 0.30326533 2: 0.07581633 3: 0.01263606 4: 0.00157952 5: 0.00015795 6: 0.00001316 7: 0.00000094 8: 0.00000006
也就是說桶中節點數量達到8的概率爲0.00000006,已經幾乎是不可能事件了,所以採用8這個數字作爲樹化閾值。
-
-
table數組的類型爲Node,但是他也有可能存放TreeNode的根節點,TreeNode並非直接繼承Node,而是
-
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> static class Entry<K,V> extends HashMap.Node<K,V>
也就是說其實TreeNode是Node的孫子而非兒子
-
-
hash操作爲取鍵的hashcode方法的返回值記爲h,然後h ^ (h >>> 16)取得最終哈希碼,即保留高16位並將低16位與高16異或的結果作爲低16位
-
取得桶的位置的方式爲(n - 1) & hash,其中n爲table的大小,很好理解,假設table大小爲16,那n - 1就是15,15&任意一個數的結果都不會超過15,所以就不會造成數組越界
-
初始容量可以自己設置,比如設置15,但是他會計算出一個大於15且是2的整數冪的數存在threshold變量中,該變量代表下一次resize時的數組大小,由於HashMap會在第一次使用時才創建table,而創建table時會調用resize方法,所以即便一開始傳入的初始容量不是2的整數冪,也依然會創建一個符合HashMap建議的table
-
之所以HashMap要求table容量一直是2的整數冪,是因爲在計算桶的位置時,採用的方式是(n - 1) & hash,2的整數冪減去1後的二進制應該是全1,這樣一來再進行與運算,可以保證充分的散列,減少hash碰撞,使元素均勻的落到table中
-
java.util.LinkedHashMap
- 線程不安全
- 繼承於HashMap
- TreeNode繼承於HashMap.Node,增加了before和after字段
- accessOrder字段標記了是訪問順序還是插入順序
- 總體跟HashMap差不多
java.util.HashTable
- 線程安全
- 使用類型爲Entry的數組table來存儲數據
- 默認初始容量爲11,默認負載因子爲0.75
- hash算法爲計h爲鍵的hashcode方法的返回值,index = (hash & 0x7FFFFFFF) % tab.length,其中0x7FFFFFFF爲首位爲0其他位都爲1的數,即整數最大值,通過取餘防止數組越界
- 通過synchronized方法保證同步線程安全
- 當元素個數超過容量*負載因子時,會觸發rehash方法
- 擴容爲原來的二倍+1
- 擴容後重新計算hash並存入新的數組中
- 沒有繼承AbstractMap而是繼承與Dictionary,但實現了Map接口
- 同樣採用鏈地址法解決衝突
- iterator遍歷過程中其他線程對Hashtable的put、 remove、clear操作都會被成功執行,所以現在並不推薦使用hashtable
java.util.concurrent.ConcurrentHashMap
-
線程安全
-
大部分成員變量與HashMap一樣,新增的爲:
-
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; /** *數組最大可能長度,toArray和一些方法會用到 */
-
DEFAULT_CONCURRENCY_LEVEL = 16; /** *併發級別,爲了兼容性從之前版本遺留下來的,因爲JDK1.8之前ConccurentHashMap保存的是一個個Segment,這個值設定的就是Segment的數量,也就是說設置爲16之後,就有16個Segment,而Segment數組一旦初始化後是不能進行擴容的,所以他就會一直保持16的並行度,也就是說最多同時允許16個線程執行安全的併發寫操作,當然這是在每一個線程操作的Segment都不同的基礎上。 */
-
MIN_TRANSFER_STRIDE = 16; /** *重新綁定每一個轉換步驟的最小值 */
-
RESIZE_STAMP_BITS = 16; /** *sizeCtl中用於生成戳的位數,32位的數組至少爲6 */
-
MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; /** *幫助調整大小的線程的數量 */
-
RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; /** *sizeCtl中記錄大小標誌的位變換 */
-
int MOVED = -1; //表明當前正在擴容中,當前的節點元素已經被轉移到新table中,頭元素hash = -1。 int TREEBIN = -2; //表示當前的桶是一個紅黑二叉樹桶,頭元素hash = -2。 int RESERVED = -3; //一般用於當key對應的值缺失需要計算的場景,在計算出新值之前臨時佔坑位用的,計算出來之後就用普通Node節點替換掉,頭元素hash = -3。 int HASH_BITS = 0x7fffffff; //正常節點哈希的可用位
-
-
桶列表table默認初始大小爲16,最大爲2^31,負載因子爲0.75,當桶中普通鏈表的元素數量超過8個就會轉成紅黑樹,當桶中紅黑樹的元素減少到6個就會轉成普通的單鏈表形式。在擴容的過程中,每個線程轉移數據的索引數量步伐爲
Max(NCPU > 1 ? (n >>> 3) / NCPU : n, 16)
,最小值爲16,其中NCPU就是CPU的核心數。 -
對於table大小爲n的表格,其散列計算方法爲
((hash^(hash >>> 16))&0X7FFFFFFF) & n
,其中n爲2的冪值(n = 2^x
) -
ConcurrentHashMap使用的鎖分段技術,首先將數據分成一段一段的存儲**,**然後給每一段數據配一把鎖,當一個線程佔用鎖修改其中一個段數據的時候,其他段的數據也能被其他線程訪問
-
ConcurrentHashMap中頻繁使用到了UnSafe類中的native方法,主要用到的有
Unsafe.putObjectVolatile(obj,long,obj2)
、Unsafe.getObjectVolatile
、Unsafe.putOrderedObject
等,在這些本地方法中,有write_barrier
和read_barrier
這兩個內存屏障,對應的就是硬件的寫屏障和讀屏障,JMM中的LoadLoad、LoadStore、StoreStore、StoreLoad四個內存屏障就是基於這兩個內存屏障做的 -
ConcurrentHashMap中保存了一個Segment類型的數組,Segment類繼承自ReentrantLock來實現加鎖,所以每次鎖住的就是數組中的一個Segment。
-
每一個Segement中保存了一個HashEntry類型的數組,這就基本對應到了HashMap中的table了。其中還有一個字段爲MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;用來保存在scanAndLockForPut方法中自旋獲取鎖的最大自選次數
-
scanAndLockForPut方法中,會用一個while循環嘗試獲取鎖,每次循環會把初始值爲-1的retries變量+1,如果retries>MAX_SCAN_RETRIES,就直接進入阻塞狀態,如果嘗試獲取鎖失敗,就會遍歷一遍對應的鏈表,找到需要put的所在位置,這樣可以把遍歷過的entry都放入高速緩存中,當獲取到鎖時再次定位就會非常高效。
-
在put方法中,會首先嚐試獲取鎖,如果沒有獲取到就會調用scanAndLockForPut獲取鎖,由於table本身被volatile關鍵字修飾,而且put方法已經加了鎖,所以在put方法中的變量都沒有加volatile關鍵字,這是如果加了volatile的話編譯器就無法對這些變量涉及到的代碼進行優化,所以在put方法中將table賦值給了一個局部變量,也是爲了這樣的優化提升性能
-
在jdk1.8中,也引入了紅黑樹,而且取消掉了Segement數組,變成了直接對Node進行加鎖,這裏加鎖就直接採用了synchronized塊進行加鎖,雖說這玩意被優化過(鎖膨脹技術),但是總感覺效率是不如juc包中的鎖的。
java.util.TreeMap
-
線程不安全
-
成員變量
-
final Comparator<? super K> comparator;//保存了鍵的比較器
-
Entry<K,V> root;//紅黑樹根節點
-
-
數據結構就是一顆紅黑樹,沒什麼好說的,需要注意的一點是Entry中還保存了一個parent屬性,可以通過孩子節點找到父節點
Set
java.util.HashSet
-
線程不安全
-
成員變量
-
HashMap<E,Object> map;//保存了一個HashMap
-
Object PRESENT = new Object();//存放到HashMap中的Value值
-
-
基本所有方法都是調用了HashMap的方法,add方法中調用map.put方法,鍵就是鍵,傳進去的值是PRESENT。
-
其他什麼容量擴容之類的都與HashMap保持一致
java.util.LinkedHashSet
-
線程不安全
-
記錄了前一個和後一個節點
-
繼承自HashSet,而且除了迭代器以外全部用的是HashSet的方法,而HashSet中保存的是一個HashMap,HashMap如何記錄前後節點呢?在HashSet的構造方法中,有這樣一個重載
-
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
實際上LinkedHashSet的構造方法中就是調用了父類HashSet的這個重載後的構造,這個構造將自己保存的HashMap創建成了LinkedHashMap(LinkedHashMap繼承自HashMap),所以自然就可以保存前後關係啦。
-
java.util.TreeSet
-
線程不安全
-
成員變量
-
NavigableMap<E,Object> m;//保存了一個可導航的Map
-
Object PRESENT = new Object();//跟其他Set一樣保存了一個空的值對象
-
-
在構造方法中給m創建了TreeMap對象,並且基本所有方法都是直接調用的TreeMap中的方法。