日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

面试必会系列 - 1.2 Java 集合,源码讲解

發布時間:2024/2/28 java 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 面试必会系列 - 1.2 Java 集合,源码讲解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文已收錄至 github,完整圖文:https://github.com/HanquanHq/MD-Notes

容器

連老師在公開課里面講過相關的源碼

Collection

  • List

    • CopyOnWriteList:讀時不加鎖,寫時復制,適用于讀線程多,寫線程少的情況
    • Vector, Stack
    • ArrayList:會有并發問題
    • LinkedList
  • Set

    • HashSet, LinkedHashSet
    • SortedSet, TreeSet
    • EnumSet
    • CopyOnWriteArraySet
    • ConcurrentSkipListSet
  • Queue

    Queue和List的區別?Queue提供了很多在多線程訪問下比較友好的API:

    add, offer, peek, poll, remove

    • Deque
      • ArrayDeque
      • BlockingDeque, LinkedBlockingDequeue
    • BlockingQueue:增加了更多API,比如put,take,可以阻塞/指定時間等待,是MQ的基礎,MQ的本質,就是一個大型的生產者、消費者模型
      • ArrayBlockingQueue
      • PriorityBlockingQueue:阻塞的 PriorityQueue
      • LinkedBlockingQueue:用鏈表實現的BlockingQueue。阻塞用await()實現,底層是park
      • TransferQueue, LinkedTransferQueue:裝完之后阻塞等待,直到有線程把它取走,再離開。場景:確認收錢完成之后,才能把商品取走。經典的 交替打印 面試題可以用 TransferQueue 實現
      • SynchronousQueue:容量為0,不能往里裝東西,只有有一個線程等著的時候,才能把東西遞到這個線程手里,是用來一個線程給另外一個線程傳數據的。和Exchanger比較相似,也是需要兩個線程同步對接,否則都會阻塞。在線程池里面,線程之間進行任務調度的時候,經常會用到。
    • PriorityQueue:內部進行了排序,底層是一個二叉樹(小頂堆)的結構
    • ConcurrentLinkedQueue:里面很多方法是CAS實現的
    • DelayQueue:是一種阻塞的隊列,需要實現compareTo方法,需要指定等待時間,用來按時間進行任務調度
  • Map

    早期的 Vector 和 Hashtable 都自帶鎖,設計上有不完善的地方,基本上不用

    • HashMap, LinkedHashMap:多線程用 HashMap 要加鎖
    • Hashtable:自帶 synchronized,線程安全,但在線程競爭激烈的情況下效率非常低下。HashTable被認為是個遺留的類。如果你尋求在迭代的時候修改Map,你應該使用ConcurrentHashMap
    • ConcurrentHashMap:CAS操作,多線程讀取的效率非常高
    • TreeMap:不存在ConcurrentTreeMap,但為了排序,用跳表ConcurrentSkipListMap代替樹結構
    • WeakHashMap
    • IdentityHashMap

HashMap

HashMap數據結構

1.8 數組+鏈表+紅黑樹

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-t7p0Aw9z-1597424575870)(images/2.png)]

JDK1.8 HashMap為什么在鏈表長度為8的時候轉紅黑樹,為啥不能是9是10?

是不是隨便什么情況下只要滿足了鏈表長度為8就轉紅黑樹呢?答案自然不是,為什么不是,看代碼:

/*** The smallest table capacity for which bins may be treeified.* (Otherwise the table is resized if too many nodes in a bin.)* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts* between resizing and treeification thresholds.*/static final int MIN_TREEIFY_CAPACITY = 64; // 當哈希表的容量>該值時,才允許將鏈表轉成紅黑樹/*** Replaces all linked nodes in bin at index for given hash unless* table is too small, in which case resizes instead.*/final void treeifyBin(Node<K,V>[] tab, int hash) {int n, index; Node<K,V> e;if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)resize();......}

這是HashMap轉紅黑樹的方法代碼,可以看到,如果此時的HashMap的長度是小于MIN_TREEIFY_CAPACITY的或者為空,則進行擴容操作,而不是轉紅黑樹,這其實也是容易忽略的點。

為什么要轉紅黑樹?

在JDK8里面,HashMap的底層數據結構已經變為數組+鏈表+紅黑樹的結構了,因為在hash沖突嚴重的情況下,鏈表的查詢效率是O(n),所以JDK8做了優化對于單個鏈表的個數大于8的鏈表,會直接轉為紅黑樹結構算是以空間換時間,這樣以來查詢的效率就變為O(logN)

為什么不直接使用紅黑樹,而是要先使用鏈表實在不行再轉紅黑樹呢?

答案自然要在源碼和注釋里找:在HashMap類中第174行左右有描述:

Because TreeNodes are about twice the size of regular nodes, weuse them only when bins contain enough nodes to warrant use(see TREEIFY_THRESHOLD)

“因為樹節點的大小是鏈表節點大小的兩倍,所以只有在容器中包含足夠的節點保證使用才用它”,顯然盡管轉為樹使得查找的速度更快,但是在節點數比較小的時候,此時對于紅黑樹來說內存上的劣勢會超過查找等操作的優勢,自然使用鏈表更加好。但是在節點數比較多的時候,綜合考慮,紅黑樹比鏈表要好。

為什么是8,而不是9不是10?

其實當時想回答面試官這是基于統計的結果,但是心里很虛還是沒有說,再回頭看看源碼的描述:

Ideally, under random hashCodes, the frequency of nodes in bins follows a Poisson distribution with a parameter of about 0.5 on average for the default resizing threshold of 0.75, although with a large variance because of resizing granularity. Ignoring variance, the expected occurrences of list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The first values are: 理想情況下,在隨機哈希碼下,bin中節點的頻率遵循泊松分布,參數平均約為0.5,默認大小調整閾值為0.75,盡管由于粒度調整的原因方差很大。忽略方差,列表大小k的期望出現次數為(exp(-0.5) * pow(0.5, k) / factorial(k))。第一個值是:0: 0.606530661: 0.303265332: 0.075816333: 0.012636064: 0.001579525: 0.000157956: 0.000013167: 0.000000948: 0.00000006more: less than 1 in ten million

理想情況下,在隨機哈希碼下,哈希表中節點的頻率遵循泊松分布,而根據統計,忽略方差,列表長度為K的期望出現的次數是以上的結果,可以看到其實在為8的時候概率就已經很小了,再往后調整意義并不大。

擴容原理

負載因子:0.75,達到這個容量,則進行 2 倍擴容,復制過去

初始容量:16

初始容量為 2 的 n 次冪:為了方便哈希時進行按位與的取模運算,計算下標位置

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-I9Eopmbc-1597424575876)(images/jdk8-hashmap-put.jpg)]

ConcurrentHashMap

幾個參數:

  • 默認大小:16
  • 負載因子:0.75
  • 默認并發級別:16
  • put 方法調用的是 Unsafe 類的 CAS 操作

結構變化:

JDK 1.7

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-TtCT0I9L-1597424575882)(images/image-20200405151029416.png)]

JDK 1.7 的 ConcurrentHashMap 擴容

HashMap是線程不安全的,我們來看下線程安全的ConcurrentHashMap,在JDK7的時候,這種安全策略采用的是 分段鎖 的機制,將數據分成一段一段的存儲,給每一段數據配一把鎖。一個線程訪問其中一個段數據時,其他段數據能被其他線程訪問。具體實現如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-e4ahWge2-1597424575888)(images/image-20200731105418097.png)]

ConcurrentHashMap維護了一個Segment數組,Segment這個類繼承了重入鎖ReentrantLock,并且該類里面維護了一個 HashEntry<K,V>[] table數組,在寫操作put,remove,擴容的時候,會對Segment加鎖,所以僅僅影響這個Segment,不同的Segment還是可以并發的,所以解決了線程的安全問題,同時又采用了分段鎖也提升了并發的效率。

JDK 1.8 的 ConcurrentHashMap 擴容

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4JlwYLLi-1597424575891)(images/d6ca7bcb0a46f21f603cbd1a488fb9660d33aea6.png)]

在JDK8中徹底拋棄了JDK7的分段鎖的機制,新的版本主要使用了Unsafe類的CAS自旋賦值+synchronized同步+LockSupport阻塞等手段實現的高效并發,代碼可讀性稍差。

ConcurrentHashMap的JDK8與JDK7版本的并發實現相比,最大的區別在于JDK8的鎖粒度更細,理想情況下talbe數組元素的大小就是其支持并發的最大個數,在JDK7里面最大并發個數就是Segment的個數,默認值是16,可以通過構造函數改變一經創建不可更改,這個值就是并發的粒度,每一個segment下面管理一個table數組,加鎖的時候其實鎖住的是整個segment,這樣設計的好處在于數組的擴容是不會影響其他的segment的,簡化了并發設計,不足之處在于并發的粒度稍粗,所以在JDK8里面,去掉了分段鎖,將鎖的級別控制在了更細粒度的table元素級別,也就是說只需要鎖住這個鏈表的head節點,并不會影響其他的table元素的讀寫,好處在于并發的粒度更細,影響更小,從而并發效率更好,但不足之處在于并發擴容的時候,由于操作的table都是同一個,不像JDK7中分段控制,所以這里需要等擴容完之后,所有的讀寫操作才能進行,所以擴容的效率就成為了整個并發的一個瓶頸點,好在Doug lea大神對擴容做了優化,本來在一個線程擴容的時候,如果影響了其他線程的數據,那么其他的線程的讀寫操作都應該阻塞,但Doug lea說你們閑著也是閑著,不如來一起參與擴容任務,這樣人多力量大,辦完事你們該干啥干啥,別浪費時間,于是在JDK8的源碼里面就引入了一個ForwardingNode類,在一個線程發起擴容的時候,就會改變sizeCtl這個值,其含義如下:

sizeCtl:0(默認): 用來控制table的初始化和擴容操作,具體應用在后續會體現出來。 -1 : 代表table正在初始化 -N : 表示有N-1個線程正在進行擴容操作 其余情況: 1、如果table未初始化,表示table需要初始化的大小。 2、如果table初始化完成,表示table的容量,默認是table大小的0.75倍

擴容時候會判斷這個值,如果超過閾值就要擴容,首先根據運算得到需要遍歷的次數i,然后利用tabAt方法獲得i位置的元素f,初始化一個forwardNode實例fwd,如果f == null,則在table中的i位置放入fwd,否則采用頭插法的方式把當前舊table數組的指定任務范圍的數據給遷移到新的數組中,然后
給舊table原位置賦值fwd。直到遍歷過所有的節點以后就完成了復制工作,把table指向nextTable,并更新sizeCtl為新數組大小的0.75倍 ,擴容完成。在此期間如果其他線程的有讀寫操作都會判斷head節點是否為forwardNode節點,如果是就幫助擴容。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-osF0BH1b-1597424575895)(images/u=3502561318,2801799575&fm=26&gp=0.jpg)]

擴容源碼如下:

/** (ConcurrentHashMap.java)* Moves and/or copies the nodes in each bin to new table. See* above for explanation.*/ private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
put 操作具體步驟:
  • 計算key的hash值,即調用speed()方法計算hash值;

  • 獲取hash值對應的Node節點位置,此時通過一個循環實現。有以下幾種情況:

    • 如果table表為空,則首先進行初始化操作,初始化之后再次進入循環獲取Node節點的位置;
    • 如果table不為空
      • 如果沒有找到key對應的Node節點,則直接調用casTabAt()方法插入一個新節點,此時不用加鎖;
      • 如果key對應的Node節點也不為空,但Node頭結點的hash值為MOVED(-1),則表示需要擴容,此時調用helpTransfer()方法擴容;

    其他情況下,則直接向Node中插入一個新Node節點,此時需要對這個Node鏈表或紅黑樹通過synchronized加鎖。

  • 插入元素后,判斷對應的Node結構是否需要改變結構,如果需要,則調用treeifyBin()方法將Node鏈表升級為紅黑樹結構;

  • 最后,調用addCount()方法記錄table中元素的數量。

  • 在擴容時,讀寫操作如何進行?

    (1)對于get讀操作,如果當前節點有數據,還沒遷移完成,此時不影響讀,能夠正常進行。 如果當前鏈表已經遷移完成,那么頭節點會被設置成fwd節點,此時get線程會幫助擴容。

    (2)對于put/remove寫操作,如果當前鏈表已經遷移完成,那么頭節點會被設置成fwd節點,此時寫線程會幫助擴容,如果擴容沒有完成,當前鏈表的頭節點會被鎖住,所以寫線程會被阻塞,直到擴容完成。

    對于 size 和迭代器是弱一致性

    volatile修飾的數組引用是強可見的,但是其元素卻不一定,所以,這導致size的根據sumCount的方法并不準確。

    同理Iteritor的迭代器也一樣,并不能準確反映最新的實際情況 .

    Hastable / ConcurrentHashMap 對比

    Hashtable 的任何操作都會把整個表鎖住,是阻塞的。

    • 好處:能獲取最實時的更新,比如說線程A調用putAll寫入大量數據,期間線程B調用get,線程B就會被阻塞,直到線程A完成putAll,因此線程B肯定能獲取到線程A寫入的完整數據。

    • 壞處:是所有調用都要排隊,效率較低。

    ConcurrentHashMap 是設計為非阻塞的。在更新時會局部鎖住某部分數據,但不會把整個表都鎖住。同步讀取操作則是完全非阻塞的。

    • 好處:處是在保證合理的同步前提下,效率很高。

    • 壞處:是嚴格來說讀取操作不能保證反映最近的更新。

    例如線程A調用putAll寫入大量數據,期間線程B調用get,則只能get到目前為止已經順利插入的部分數據。

    ConcurrentHashMap 總結

    Java7 中 ConcruuentHashMap 使用的分段鎖,也就是每一個 Segment 上同時只有一個線程可以操作,每一個 Segment 都是一個類似 HashMap 數組的結構,它可以擴容,它的沖突會轉化為鏈表。但是 Segment 的個數一但初始化就不能改變。

    Java8 中的 ConcruuentHashMap 使用的 Synchronized 鎖加 CAS 的機制。結構也由 Java7 中的 Segment 數組 + HashEntry 數組 + 鏈表 進化成了 Node 數組 + 鏈表 / 紅黑樹,Node 是類似于一個 HashEntry 的結構。它的沖突再達到一定大小時會轉化成紅黑樹,在沖突小于一定數量時又退回鏈表。

    有些同學可能對 Synchronized 的性能存在疑問,其實 Synchronized 鎖自從引入鎖升級策略后,性能不再是問題,有興趣的同學可以自己了解下 Synchronized 的鎖升級

    總結

    以上是生活随笔為你收集整理的面试必会系列 - 1.2 Java 集合,源码讲解的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。