類集框架面試

類集框架面試

List

java.util.Vector
  1. Vector對幾乎所有方法都加了鎖,且大部分都是synchronized方法,包括get方法,鎖的粒度較粗,性能較差,在jdk1.0版本就已經發布,很老舊的類,目前Java已經不推薦使用。

  2. Vector初始默認容量爲10,默認每次擴容爲原來的一倍,可手動指定擴容大小

  3. 數據存放在一個Object類型的數組中

  4. 線程安全

java.util.ArrayList
  1. 線程不安全

  2. 默認初始容量爲10,擴容每次增加原來的一半

    • oldCapacity + (oldCapacity >> 1)
      
  3. 數據存儲在Object類型的名爲elementData的數組中,除此之外還有名爲EMPTY_ELEMENTDATA的Object空數組用於空實例共享,名爲DEFAULTCAPACITY_EMPTY_ELEMENTDATA的Object類型空數組用於默認大小的空實例的共享空數組實例。我們將其與EMPTY_ELEMENTDATA區分開來,以知道在添加第一個元素時要膨脹多少。

  4. 默認數組最大長度爲Integer.MAX_VALUE - 8,這是因爲有些虛擬機需要在數組中保留一些頭字,如果嘗試分配較大的數組可能會導致OutOfMemoryError

  5. 每次擴容時會傳遞一個名爲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
  1. 線程不安全

  2. 雙向鏈表,存儲結構爲一個私有的靜態內部類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;
          }
      }
      
  3. jdk1.6後加入了descendingIterator方法返回迭代器用於逆向遍歷

  4. 添加元素時int類型的size會++,但是源碼中並沒有考慮溢出的情況,沒有判斷也沒有拋出任何異常

java.util.concurrent.CopyOnWriteArrayList
  1. 線程安全(廢話,JUC包下的)
  2. 使用ReentrantLock加鎖
  3. 使用volatile修飾的Object類型的數組
  4. 默認創建空數組
  5. 讀不加鎖寫加鎖,且都是在finally中釋放鎖
  6. 執行寫操作時會調用Arrays.copyOf方法創建新數組,並在新數組中寫,寫後將原引用替換爲新數組,所有操作都在加鎖情況下進行,這樣確定不會頻繁觸發gc?

Map

java.util.HashMap
  1. 線程不安全

  2. 成員變量

    1. DEFAULT_INITIAL_CAPACITY = 1 << 4;
      /**
      *初始容量爲16,必須爲2的整數冪,原因後面會說
      */
      
    2. MAXIMUM_CAPACITY = 1 << 30;
      /**
      *最大容量2的30次方
      */
      
    3. DEFAULT_LOAD_FACTOR = 0.75f;
      /**
      *默認負載因子
      */
      
    4. TREEIFY_THRESHOLD = 8;
      /**
      *樹化閾值,該值必須大於2,且至少應爲8,纔可以符合轉化回鏈表的一個假設
      */
      
    5. UNTREEIFY_THRESHOLD = 6;
      /**
      *轉回鏈表的閾值,應小於TREEIFY_THRESHOLD且不超過6
      */
      
    6. MIN_TREEIFY_CAPACITY = 64;
      /**
      *最小樹化閾值,只有當哈希表容量大於該值,才允許進行樹化,否則會進行一次擴容
      */
      
    7. static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;
          V value;
          Node<K,V> next;
      }
      /**
      *hash表基本結構
      */
      
    8. Node<K,V>[] table;
      /**
      *存放每一根鏈表的數組,在第一次使用時初始化
      */
      
    9. Set<Map.Entry<K,V>> entrySet;
      /**
      *鍵值對集合,沒什麼好說的
      */
      
    10. table數組會在第一次使用時初始化,第一次使用時會調用resize方法,在resize方法中會進行創建數組等操作

    11. 之所以使用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這個數字作爲樹化閾值。

    12. 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的孫子而非兒子

    13. hash操作爲取鍵的hashcode方法的返回值記爲h,然後h ^ (h >>> 16)取得最終哈希碼,即保留高16位並將低16位與高16異或的結果作爲低16位

    14. 取得桶的位置的方式爲(n - 1) & hash,其中n爲table的大小,很好理解,假設table大小爲16,那n - 1就是15,15&任意一個數的結果都不會超過15,所以就不會造成數組越界

    15. 初始容量可以自己設置,比如設置15,但是他會計算出一個大於15且是2的整數冪的數存在threshold變量中,該變量代表下一次resize時的數組大小,由於HashMap會在第一次使用時才創建table,而創建table時會調用resize方法,所以即便一開始傳入的初始容量不是2的整數冪,也依然會創建一個符合HashMap建議的table

    16. 之所以HashMap要求table容量一直是2的整數冪,是因爲在計算桶的位置時,採用的方式是(n - 1) & hash,2的整數冪減去1後的二進制應該是全1,這樣一來再進行與運算,可以保證充分的散列,減少hash碰撞,使元素均勻的落到table中

java.util.LinkedHashMap
  1. 線程不安全
  2. 繼承於HashMap
  3. TreeNode繼承於HashMap.Node,增加了before和after字段
  4. accessOrder字段標記了是訪問順序還是插入順序
  5. 總體跟HashMap差不多
java.util.HashTable
  1. 線程安全
  2. 使用類型爲Entry的數組table來存儲數據
  3. 默認初始容量爲11,默認負載因子爲0.75
  4. hash算法爲計h爲鍵的hashcode方法的返回值,index = (hash & 0x7FFFFFFF) % tab.length,其中0x7FFFFFFF爲首位爲0其他位都爲1的數,即整數最大值,通過取餘防止數組越界
  5. 通過synchronized方法保證同步線程安全
  6. 當元素個數超過容量*負載因子時,會觸發rehash方法
  7. 擴容爲原來的二倍+1
  8. 擴容後重新計算hash並存入新的數組中
  9. 沒有繼承AbstractMap而是繼承與Dictionary,但實現了Map接口
  10. 同樣採用鏈地址法解決衝突
  11. iterator遍歷過程中其他線程對Hashtable的put、 remove、clear操作都會被成功執行,所以現在並不推薦使用hashtable
java.util.concurrent.ConcurrentHashMap
  1. 線程安全

  2. 大部分成員變量與HashMap一樣,新增的爲:

    1. MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
      /**
      *數組最大可能長度,toArray和一些方法會用到
      */
      
    2. DEFAULT_CONCURRENCY_LEVEL = 16;
      /**
      *併發級別,爲了兼容性從之前版本遺留下來的,因爲JDK1.8之前ConccurentHashMap保存的是一個個Segment,這個值設定的就是Segment的數量,也就是說設置爲16之後,就有16個Segment,而Segment數組一旦初始化後是不能進行擴容的,所以他就會一直保持16的並行度,也就是說最多同時允許16個線程執行安全的併發寫操作,當然這是在每一個線程操作的Segment都不同的基礎上。
      */
      
    3. MIN_TRANSFER_STRIDE = 16;
      /**
      *重新綁定每一個轉換步驟的最小值
      */
      
    4. RESIZE_STAMP_BITS = 16;
      /**
      *sizeCtl中用於生成戳的位數,32位的數組至少爲6
      */
      
    5. MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
      /**
      *幫助調整大小的線程的數量
      */
      
    6. RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
      /**
      *sizeCtl中記錄大小標誌的位變換
      */
      
    7. int MOVED = -1; //表明當前正在擴容中,當前的節點元素已經被轉移到新table中,頭元素hash = -1。
      int TREEBIN = -2; //表示當前的桶是一個紅黑二叉樹桶,頭元素hash = -2。
      int RESERVED = -3; //一般用於當key對應的值缺失需要計算的場景,在計算出新值之前臨時佔坑位用的,計算出來之後就用普通Node節點替換掉,頭元素hash = -3。
      int HASH_BITS = 0x7fffffff; //正常節點哈希的可用位
      
  3. 桶列表table默認初始大小爲16,最大爲2^31,負載因子爲0.75,當桶中普通鏈表的元素數量超過8個就會轉成紅黑樹,當桶中紅黑樹的元素減少到6個就會轉成普通的單鏈表形式。在擴容的過程中,每個線程轉移數據的索引數量步伐爲Max(NCPU > 1 ? (n >>> 3) / NCPU : n, 16),最小值爲16,其中NCPU就是CPU的核心數。

  4. 對於table大小爲n的表格,其散列計算方法爲((hash^(hash >>> 16))&0X7FFFFFFF) & n,其中n爲2的冪值(n = 2^x)

  5. ConcurrentHashMap使用的鎖分段技術,首先將數據分成一段一段的存儲**,**然後給每一段數據配一把鎖,當一個線程佔用鎖修改其中一個段數據的時候,其他段的數據也能被其他線程訪問

  6. ConcurrentHashMap中頻繁使用到了UnSafe類中的native方法,主要用到的有Unsafe.putObjectVolatile(obj,long,obj2)Unsafe.getObjectVolatileUnsafe.putOrderedObject等,在這些本地方法中,有write_barrierread_barrier這兩個內存屏障,對應的就是硬件的寫屏障和讀屏障,JMM中的LoadLoad、LoadStore、StoreStore、StoreLoad四個內存屏障就是基於這兩個內存屏障做的

  7. ConcurrentHashMap中保存了一個Segment類型的數組,Segment類繼承自ReentrantLock來實現加鎖,所以每次鎖住的就是數組中的一個Segment。

  8. 每一個Segement中保存了一個HashEntry類型的數組,這就基本對應到了HashMap中的table了。其中還有一個字段爲MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;用來保存在scanAndLockForPut方法中自旋獲取鎖的最大自選次數

  9. scanAndLockForPut方法中,會用一個while循環嘗試獲取鎖,每次循環會把初始值爲-1的retries變量+1,如果retries>MAX_SCAN_RETRIES,就直接進入阻塞狀態,如果嘗試獲取鎖失敗,就會遍歷一遍對應的鏈表,找到需要put的所在位置,這樣可以把遍歷過的entry都放入高速緩存中,當獲取到鎖時再次定位就會非常高效。

  10. 在put方法中,會首先嚐試獲取鎖,如果沒有獲取到就會調用scanAndLockForPut獲取鎖,由於table本身被volatile關鍵字修飾,而且put方法已經加了鎖,所以在put方法中的變量都沒有加volatile關鍵字,這是如果加了volatile的話編譯器就無法對這些變量涉及到的代碼進行優化,所以在put方法中將table賦值給了一個局部變量,也是爲了這樣的優化提升性能

  11. 在jdk1.8中,也引入了紅黑樹,而且取消掉了Segement數組,變成了直接對Node進行加鎖,這裏加鎖就直接採用了synchronized塊進行加鎖,雖說這玩意被優化過(鎖膨脹技術),但是總感覺效率是不如juc包中的鎖的。

java.util.TreeMap
  1. 線程不安全

  2. 成員變量

    1. final Comparator<? super K> comparator;//保存了鍵的比較器
      
    2. Entry<K,V> root;//紅黑樹根節點
      
  3. 數據結構就是一顆紅黑樹,沒什麼好說的,需要注意的一點是Entry中還保存了一個parent屬性,可以通過孩子節點找到父節點

Set

java.util.HashSet
  1. 線程不安全

  2. 成員變量

    1. HashMap<E,Object> map;//保存了一個HashMap
      
    2. Object PRESENT = new Object();//存放到HashMap中的Value值
      
  3. 基本所有方法都是調用了HashMap的方法,add方法中調用map.put方法,鍵就是鍵,傳進去的值是PRESENT。

  4. 其他什麼容量擴容之類的都與HashMap保持一致

java.util.LinkedHashSet
  1. 線程不安全

  2. 記錄了前一個和後一個節點

  3. 繼承自HashSet,而且除了迭代器以外全部用的是HashSet的方法,而HashSet中保存的是一個HashMap,HashMap如何記錄前後節點呢?在HashSet的構造方法中,有這樣一個重載

    1. HashSet(int initialCapacity, float loadFactor, boolean dummy) {
          map = new LinkedHashMap<>(initialCapacity, loadFactor);
      }
      

    實際上LinkedHashSet的構造方法中就是調用了父類HashSet的這個重載後的構造,這個構造將自己保存的HashMap創建成了LinkedHashMap(LinkedHashMap繼承自HashMap),所以自然就可以保存前後關係啦。

java.util.TreeSet
  1. 線程不安全

  2. 成員變量

    1. NavigableMap<E,Object> m;//保存了一個可導航的Map
      
    2. Object PRESENT = new Object();//跟其他Set一樣保存了一個空的值對象
      
  3. 在構造方法中給m創建了TreeMap對象,並且基本所有方法都是直接調用的TreeMap中的方法。

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