TreeMap源碼 非線程安全 (結合synchronizedMap()可變爲線程安全)
- 繼承於AbstractMap[k-v集合],實現了NavigableMap接口【支持一系列的導航方法getFirstEntry】、Cloneable接口、Serializable接口
- 基於紅黑樹進行排序,key比較大小是根據比較器comparator來進行判斷;紅黑樹節點是Entry類型
- TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。
- TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的
Entry
Entry是紅黑數的節點,它包含了紅黑數的6個基本組成成分:key(鍵)、value(值)、left(左孩子)、right(右孩子)、parent(父節點)、color(顏色)
Entry節點根據key進行排序,Entry節點包含的內容爲value
TreeMap的Entry相關函數 NavigableMap接口
firstEntry()、 lastEntry()、 lowerEntry()、 higherEntry()、 floorEntry()、 ceilingEntry()、 pollFirstEntry() 、 pollLastEntry()
firstEntry() 和 getFirstEntry() 都是用於獲取第一個節點
firstEntry() 是對外接口,返回的Entry不能被修改; getFirstEntry() 是內部接口,返回的Entry可以被修改
還有key、value
TreeMap的entrySet()函數
- contains(Object o):比較equals比較是否有相同的value
- remove(Object o):類似contains如果有相同,則刪除
- size(),clear
if (p != null && valEquals(p.getValue(), value)) { deleteEntry(p); return true; }
TreeMap實現java.io.Serializable,分別實現了串行讀取、寫入功能。
串行寫入函數是writeObject(),它的作用是將TreeMap的“容量,所有的Entry”都寫入到輸出流中。
而串行讀取函數是readObject(),它的作用是將TreeMap的“容量、所有的Entry”依次讀出。
readObject() 和 writeObject() 正好是一對,通過它們,我能實現TreeMap的串行傳輸。
遍歷TreeMap的鍵值對 entrySet()
第一步:根據entrySet()獲取TreeMap的“鍵值對”的Set集合
第二步:通過Iterator迭代器遍歷“第一步”得到的集合
// 假設map是TreeMap對象 // map中的key是String類型,value是Integer類型 Integer integ = null; Iterator iter = map.entrySet().iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); // 獲取key key = (String)entry.getKey(); // 獲取value integ = (Integer)entry.getValue(); }
map.keySet()獲取TreeMap的“鍵”的Set集合
map.values()獲取TreeMap的“值”的集合
順序遍歷和逆序遍歷
由於TreeMap中的元素是從小到大的順序排列的。因此,順序遍歷,就是從第一個元素開始,逐個向後遍歷;
而倒序遍歷則恰恰相反,它是從最後一個元素開始,逐個往前遍歷。
我們可以通過 keyIterator() 和 descendingKeyIterator()來實現
- keyIterator()的作用是返回順序的KEY的集合,先getFirstEntry(),再nextEntry().key
- descendingKeyIterator()的作用是返回逆序的KEY的集合。先getLastEntry(),再prevEntry().key
紅黑樹插入刪除
爲什麼要旋轉:防止樹退化成鏈,導致O(n)(而不是logn)的高度
第一步: 將紅黑樹當作一顆二叉查找樹,將節點插入
根據樹的鍵的順序進行插入,保證插入後仍然有序
第二步:將插入的節點着色爲"紅色"
(1) 每個節點或者是黑色,或者是紅色。
(2) 根節點是黑色。
(3) 每個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的葉子節點!]
(4) 如果一個節點是紅色的,則它的子節點必須是黑色的。
(5) 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
將插入的節點着色爲紅色,不會違背"特性(5)"!少違背一條特性,就意味着我們需要處理的情況越少。接下來,就要努力的讓這棵樹滿足其它性質即可;滿足了的話,它就又是一顆紅黑樹了。o(∩∩)o...哈哈
第三步: 通過一系列的旋轉或着色等操作,使之重新成爲一顆紅黑樹。
第二步中,將插入節點着色爲"紅色"之後,不會違背"特性(5)"。那它到底會違背哪些特性呢?
對於"特性(1)",顯然不會違背了。因爲我們已經將它塗成紅色了。
對於"特性(2)",顯然也不會違背。在第一步中,我們是將紅黑樹當作二叉查找樹,然後執行的插入操作。而根據二叉查找數的特點,插入操作不會改變根節點。所以,根節點仍然是黑色。
對於"特性(3)",顯然不會違背了。這裏的葉子節點是指的空葉子節點,插入非空節點並不會對它們造成影響。
對於"特性(4)",是有可能違背的!
那接下來,想辦法使之"滿足特性(4)",就可以將樹重新構造成紅黑樹了。
Java併發包 java.util.concurrent(簡稱JUC):併發包、阻塞隊列、線程池Executor、.locks 、原子類.atomic
- ConcurrentHashMap
- CopyOnWriteArrayList
- 線程安全版本的ArrayList,每次增加的時候,需要新創建一個比原來容量+1大小的數組
- 拷貝原來的元素到新的數組中,同時將新插入的元素放在最末端。
- 然後切換引用;
- 迭代時生成快照數組;適合讀多寫少
- CopyOnWriteArraySet
- 基於CopyOnWriteArrayList實現;
- 不能插入重複數據,每次add的時候都要遍歷數據,性能略低於CopyOnWriteArrayList
- 阻塞隊列:
- ArrayBlockingQueue 數組 組成的有界阻塞隊列
- synchronizedQueue 不存儲元素的BlockingQueue。每一個put操作必須要等待一個take操作,否則不能繼續添加元素;適合做交換工作
- Atomic類,如AtomicInteger、AtomicBoolean i++變成原子操作, 底層是CAS,別的線程自旋等到該方法執行完成
- locks Lock lock = new ReentrantLock();
synchronizedMap() map範圍的鎖
SynchronizedMap類是定義在集合Collections中的一個靜態內部類。
實現了Map接口,通過synchronized關鍵字對map實現同步控制,可以傳各類Map,如TreeMap,hashMap
HashMap簡介 非線程安全
JDK7-Entry數組+鏈表的方式
JDK8-Entry數組+鏈表/紅黑樹
- 鏈表的長度達到閥值 8 ,鏈表就將轉換成紅黑樹;小於6,轉鏈表;防止鏈表和樹頻繁轉換
- 實現了Serializable接口,支持序列化;實現了Cloneable接口,能被克隆
hashSet 底層也是hashMap,存儲在key上,v是個固定值,重寫equls方法判斷相同
結構
HashMap底層維護一個Entry數組,數組中的存儲Entry對象,組成的鏈表;鏈表是爲了解決Hash衝突
Map中的key,value則以Entry的形式存放在數組中,通過key的hashCode計算,允許null值,存儲在table[0]
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry是HashMap中的一個靜態內部類 static class Entry<K,V> implements Map.Entry<K,V>
final K key; V value; Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結構 int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重複計算
構造器
快速響應失敗 final float loadFactor;由於HashMap非線程安全,在對HashMap進行迭代時,如果期間其他線程的參與導致HashMap的結構發生變化了,拋出異常
默認初始容量16,因子0.75
PUT
常規構造器中,執行put操作的時候才真正構建table數組
如果空數組,分配空間
檢查key
如果key爲null,存儲位置爲table[0]或table[0]的鏈表
計算Hash,計算實際位置 i = indexFor(hash, table.length) h & (length-1);
插入操作 鏈頭插入
addEntry:
- 判斷是否需要擴容
- 判斷table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向下面;
- 遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;插入到前面;
- 遍歷中若發現key已經存在直接覆蓋value;
hash衝突(碰撞):鏈地址法; 先找到下標 i,KEY值找Entry對象,新值存放在數組中,舊值在新值的鏈表上,將存放在數組中的Entry設置爲新值的next
GET
對key進行null檢查:如果key是null,索引爲table[ i ]
key的hashcode()方法被調用,然後計算hash值。
indexFor(hash,table.length)用來計算要獲取的Entry對象在table數組中的精確的位置 index = h & (length-1)
遍歷鏈表,調用equals()方法檢查key相等性,如果equals()方法返回true,get方法返回Entry對象的value,否則,返回null。
擴容:到達加載因子時,resize(2 * table.length);
新建一個HashMap的底層數組,而後調用transfer方法,就HashMap的全部元素添加到新HashMap,
遍歷舊數組,重新計算indexFor,鏈表逆序指向新數組
紅黑樹是一種近似平衡的二叉查找樹(binary search tree) O(log2 n)
樹節點特徵:滿足如下條件:
- 每個節點要麼是紅色,要麼是黑色。
- 根節點必須是黑色
- 紅色節點不能連續(也即是,紅色節點的孩子和父親都不能是紅色)。
- 對於每個節點,從該點至葉子的任何路徑,都含有相同個數的黑色節點
- 確保節點的左右子樹的高度差,不會超過二者中較低那個的一倍
插入:
插入檢查會不會破壞樹的特徵,如果破壞了,程序就會進行糾正,根據需要改變樹的結構
GET
O(logN)來搜索一棵樹
維持平衡【旋轉】的原因:
- 解決了二叉查找樹退化成鏈表的問題
- 把插入,查找,刪除的時間複雜度最好情況和最壞情況都維持在O(logN)
與AVL數的比較:
紅黑是用近似的平衡來換取增刪節點時候旋轉次數的降低,任何不平衡都會在三次旋轉之內解決,
而AVL是嚴格平衡樹,因此在增加或者刪除節點的時候,根據不同情況,旋轉的次數比紅黑樹要多。
所以紅黑樹的增刪效率更高
HashMap共有四個構造方法 構造方法中兩個很重要的參數:初始容量16和加載因子0.75
初始容量表示哈希表的長度,初始是16
鏈表的Node節點數量 超出了加載因子與當前容量的乘積時,則要對該哈希表進行 resize 操作(即擴容) 默認0.75
加載因子太大,對空間的利用更充分,但是查找效率會降低(鏈表長度會越來越長);
加載因子太小,那麼表中的數據將過於稀疏,對空間造成嚴重浪費(很多空間還沒用,就開始擴容了)
加入鍵值對時,先判斷當前已用數組長度是否大於等於閥值(容量*加載因子),如果大於等於,則進行擴容,容量擴爲原容量2倍
散列算法 hash&(length-1) 二進制
Hashtable:key的hash值對length-1取模(即除法散列法),基本能保證元素在哈希表中散列的比較均勻,但除法運算,效率很低;
HashMap改進,&操作:通過h&(length-1)的方法來代替取模,同樣實現了均勻的散列,但效率要高很多,h = key的hash
h = 101100110,length = 1000,h%(length-1)就是保留h右邊的3位二進制=110 (也是爲什麼數組從0開始)
爲什麼數組的容量一定要是2的整數次冪
h&(length-1),散列的均勻、空間利用充足
- 2的整數次冪爲偶數,length-1爲奇數,最後一位是1,保證h&(length-1)的最後一位可能爲0或1,保證散列的均勻性,且空間利用充足
- length爲奇數的話,length-1爲偶數,最後一位是0,h&(length-1)的最後一位爲0,浪費了近一半的空間
resize方法
新建一個HashMap的底層數組,而後調用transfer方法,將就HashMap的全部元素添加到新HashMap,
要重新計算元素在新的數組中的索引位置,即重新調用indexFor
containsKey方法和containsValue方法
前者直接可以通過key的哈希值將搜索範圍定位到對應的鏈表;
後者要對哈希數組的每個鏈表進行搜索
多線程情況下不安全 擴容時候,兩個線程擴容 指針逆序導致 循環指針
- 對索引數組中的元素遍歷
- 對鏈表上的每一個節點遍歷:用 next 取得要轉移那個元素的下一個,將 e 轉移到新 Hash 表的頭部,使用頭插法插入節點。
轉移的時候是逆序的。假如轉移前鏈表順序是1->2->3,那麼轉移後就會變成3->2->1。
死鎖問題就是因爲1->2的同時2->1造成的嗎?所以,HashMap 的死鎖問題就出在這個transfer()函數上
HashMap是非線程安全,死鎖一般都是產生於併發情況下。我們假設有二個進程T1、T2,HashMap容量爲2,T1線程放入key A、B、C、D、E。在T1線程中A、B、C Hash值相同,於是形成一個鏈接,假設爲1->2->3,而4、5 Hash值不同,於是容量不足,需要新建一個更大尺寸的hash表,然後把數據從老的Hash表中
遷移到新的Hash表中(refresh)。這時T2進程闖進來了,T1暫時掛起,T2進程也準備放入新的key,這時也
發現容量不足,也refresh一把。refresh之後原來的鏈表結構假設爲3->2,之後T1進程繼續執行,鏈接結構
爲2->3,這時就形成2.next=3,3.next=2的環形鏈表。一旦取值進入這個環形鏈表就會陷入死循環。
如何減少碰撞?
使用不可變的、聲明作final的對象,並且採用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作爲鍵是非常好的選擇
hashTable:當一個線程使用put方法時,另一個線程不但不可以使用put方法,連get方法都不可以,效率低
concurrenthashmap 源碼 JDK1.8版 synchronizedMap
特點
- 1.8拋棄Segment分段鎖機制,
- 利用CAS+Synchronized來保證併發更新的安全,
- 底層依然採用數組+鏈表+紅黑樹的存儲結構
- null值空指針異常
概念
table:初始爲null,第一次put操作時初始化,默認大小爲16的數組,存儲Node節點數據,擴容時大小總是2的冪次方 加倍,直到2^31-1
nextTable:默認爲null,擴容時新生成的數組,其大小爲原數組的兩倍。
sizeCtl :默認爲0,用來控制table的初始化和擴容操作,具體應用在後續會體現出來;CAS修改控制併發問題
- -1 代表table正在初始化(保證只有一個put觸發初始化)
- -N 表示有N-1個線程正在進行擴容操作
- 其餘情況:
- 如果table未初始化,表示table需要初始化的大小16 this.sizeCtl = cap;
- 如果table初始化完成,默認是table大小的0.75倍
Node:只讀節點(不提供修改方法),構造table[]
保存key,value、key的hash值 class Node<K,V> implements Map.Entry<K,V> ;value和next都用volatile修飾,保證併發的可見性
final int hash; final K key; volatile V val;//volatile,保證可見性; volatile Node<K,V> next;
ForwardingNode:特殊的Node節點,hash值爲-1,其中存儲nextTable的引用;f頭節點爲null
table發生擴容的時候,ForwardingNode作爲一個佔位符放在table尾端,表示當前節點爲null
延遲初始化、獨佔
延遲初始化:只會初始化sizeCtl值,並不會直接初始化table,而是延緩到第一次put操作
多線程下的初始化機制:執行第一次put操作的線程會CAS方法修改sizeCtl爲-1,且只有一個線程能夠修改成功,
put操作
- 參數校驗,key value不能爲null;
- 若table[]未創建,則初始化;
- 當table[i]無鏈表無節點時,利用CAS操作直接創建Node並存儲
- 如果當前正在擴容 -1,則幫助擴容並返回最新table[]。
- 然後在鏈表或者紅黑樹中追加節點;需要對索引對象(頭)節點加同步鎖:synchronized (f)
- 判斷節點個數是否到達閥值,若爲>=8,變爲紅黑樹結構
假設table已經初始化完成,put操作採用CAS+synchronized實現併發插入或更新操作
- 定位索引位置,index = hash &(lengh - 1)
- 獲取table[i]中對應索引的對象f,node類型:Unsafe.getObjectVolatile來獲取 tabAt[index],獲取指定內存的數據,保證每次拿到數據都是最新;因爲線程都有一個工作內存,裏面存儲着table的副本,雖然table是volatile修飾的,但不能保證線程每次都拿到table中的最新元素
- 如果f爲null,說明table中該位置第一次插入元素,利用Unsafe.compareAndSwapObject(CAS)方法插入Node節點
-
- CAS成功,說明Node節點已經插入,隨後檢查是否需要進行擴容
- CAS失敗,說明有其它線程提前插入了節點,自旋重新嘗試插入節點
- 如果f的hash值爲-1,說明當前f是ForwardingNode節點,意味有其它線程正在擴容,,且當前槽位已經處理過了,則一起進行擴容操作
- f不爲null的其餘情況把新的Node節點按鏈表或紅黑樹的方式插入到合適的位置,這個過程採用同步內置鎖實現併發(Synchronized鎖住f,減少了鎖粒度)
// 獲取數組該位置的頭結點的監視器鎖 synchronized (f) {
-
- 在節點f上進行同步,節點插入之前,再次檢查節點是否改變,【利用CAS tabAt(tab, i) == f 判斷】,防止被其它線程修改
- 如果f.hash >= 0,說明f是鏈表結構的頭結點,遍歷鏈表,如果找到對應的node節點,則修改value,否則在鏈表尾部加入節點
-
- 如果f是TreeBin類型節點 if(f instanceof TreeBin),說明f是紅黑樹根節點,則在樹結構上遍歷元素,更新或增加節點
- 如果鏈表中節點數 binCount >= TREEIFY_THRESHOLD(默認是8),則把鏈表轉化爲紅黑樹結構
新增節點之後,會調用addCount方法記錄元素個數
// 數組計數增加1,有可能觸發transfer操作(擴容);當數組元素個數達到閾值時,會觸發transfer方法,重新調整節點的位置 addCount(1L, binCount);
get操作:getObjectVolatile 保證可見性獲取索引的對象f=tabAt[index],遍歷key,找到相等的,cas來保證變量的原子性讀取
table擴容
元素數量達到容量閾值sizeCtl(長度*0.75),擴容分爲兩部分:
構建一個nextTable,大小爲table的兩倍
Unsafe.compareAndSwapInt(CAS)修改sizeCtl值-1,保證只有一個線程初始化,擴容後的數組長度爲原來的兩倍,但是容量是原來的1.5(2*0.75)
把table的數據複製到nextTable中:
擴容操作支持併發插入,支持節點的併發複製,性能提升
初始化ForwardingNode節點,其中保存了新數組nextTable的引用,在處理完每個槽位的節點之後當做佔位節點,表示該槽位已經處理過了;
如果槽位15已經被線程A處理了,那麼線程B處理到這個節點時,取到該節點的hash值應該爲MOVED,值爲-1,即爲fwd節點,則直接跳過
2.構造反序列表,放入nextTable(舊數據放在數組後面,即新擴容的地方)
如果f是鏈表的頭節點,就構造一個反序鏈表,移動對應位置[需要重新計算hash],
移動完成,採用Unsafe.putObjectVolatile方法給table原位置賦值fwd節點
3.遍歷過所有的節點,把table指向nextTable,更新sizeCtl爲新數組大小的0.75
synchronizedMap()
SynchronizedMap類是定義在Collections中的一個靜態內部類。實現了Map接口,通過synchronized關鍵字對map實現同步控制
map範圍(map-wide)的鎖,保證插入、刪除或者檢索操作的完整性
區別及應用場景
1.ConcurrentHashMap的實現更加精細,在性能以及安全性方面更優
同步操作精確控制到node,其他線程,仍然可以對node執行某些操作
多個讀操作幾乎總可以併發地執行
例如:在遍歷map時,其他線程試圖對map進行數據修改,不會拋出ConcurrentModificationException【正在修改錯誤】
2.synchronizedMap()可以接收任意Map實例,實現Map的同步,如TreeMap實現排序,ConcurrentHashMap只能是HashMap
Map<String, Object> map2 = Collections.synchronizedMap(new TreeMap<String, Object>());
Map<String, Object> map3 = new ConcurrentHashMap<String, Object>();
ConcurrentLinkedQueue
- 一個基於鏈接節點的無界非阻塞線程安全隊列。此隊列按照 FIFO(先進先出)原則對元素進行排序
- ConcurrentLinkedQueue中有兩個volatile類型的Node節點分別用來存在列表的首尾節點,其中head節點存放鏈表第一個item爲null的節點,tail則並不是總指向最後一個節點
- Node節點內部則維護一個變量item用來存放節點的值,next用來存放下一個節點,從而鏈接爲一個單向無界列表
應用:多線程方位一個集合
ConcurrentLinkedQueue
使用CAS非阻塞算法實現使用CAS解決了當前節點與next節點之間的安全鏈接和對當前節點值的賦值。由於使用CAS沒有使用鎖,所以獲取size的時候有可能進行offer,poll或者remove操作,導致獲取的元素個數不精確,所以在併發情況下size函數不是很有用。另外第一次peek或者first時候會把head指向第一個真正的隊列元素。
下面總結下如何實現線程安全的,可知入隊出隊函數都是操作volatile變量:head,tail。所以要保證隊列線程安全只需要保證對這兩個Node操作的可見性和原子性,由於volatile本身保證可見性,所以只需要看下多線程下如果保證對着兩個變量操作的原子性。
對於offer操作是在tail後面添加元素,也就是調用tail.casNext方法,而這個方法是使用的CAS操作,只有一個線程會成功,然後失敗的線程會循環一下,重新獲取tail,然後執行casNext方法。對於poll也是這樣的。
offer操作
public boolean offer(E e) {
//e爲null則拋出空指針異常
checkNotNull(e);
//構造Node節點構造函數內部調用unsafe.putObject,後面統一講
final Node<E> newNode = new Node<E>(e);
//從尾節點插入
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
//如果q=null說明p是尾節點則插入
if (q == null) {
//cas插入(1)
if (p.casNext(null, newNode)) {
//cas成功說明新增節點已經被放入鏈表,然後設置當前尾節點(包含head,1,3,5.。。個節點爲尾節點)
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)//(2)
//多線程操作時候,由於poll時候會把老的head變爲自引用,然後head的next變爲新head,所以這裏需要
//重新找新的head,因爲新的head後面的節點纔是激活的節點
p = (t != (t = tail)) ? t : head;
else
// 尋找尾節點(3)
p = (p != t && t != (t = tail)) ? t : q;
}
}
poll操作
public E poll() {
restartFromHead:
//死循環
for (;;) {
//死循環
for (Node<E> h = head, p = h, q;;) {
//保存當前節點值
E item = p.item;
//當前節點有值則cas變爲null(1)
if (item != null && p.casItem(item, null)) {
//cas成功標誌當前節點以及從鏈表中移除
if (p != h) // 類似tail間隔2設置一次頭節點(2)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
//當前隊列爲空則返回null(3)
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
//自引用了,則重新找新的隊列頭節點(4)
else if (p == q)
continue restartFromHead;
else//(5)
p = q;
}
}
}
final void updateHead(Node<E> h, Node<E> p) {
if (h != p && casHead(h, p))
h.lazySetNext(h);
}
peek操作
peek操作是獲取鏈表頭部一個元素(只讀取不移除),下面看看實現原理。
代碼與poll類似,只是少了castItem.並且peek操作會改變head指向,offer後head指向哨兵節點,第一次peek後head會指向第一個真的節點元素。
public E peek() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null || (q = p.next) == null) {
updateHead(h, p);
return item;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
隊列的頭部 是隊列中時間最長的元素。隊列的尾部 是隊列中時間最短的元素。
新的元素插入到隊列的尾部,隊列獲取操作從隊列頭部獲得元素。當多個線程共享訪問一個公共 collection 時,ConcurrentLinkedQueue 是一個恰當的選擇。此隊列不允許使用 null 元素。
將指定元素插入此隊列的尾部。
poll()
獲取並移除此隊列的頭,如果此隊列爲空,則返回 null。 CAS
peek()
獲取但不移除此隊列的頭;如果此隊列爲空,則返回 null
從隊列中移除指定元素的單個實例(如果存在)
如果此隊列包含指定元素,則返回 true
如圖ConcurrentLinkedQueue中有兩個volatile類型的Node節點分別用來存在列表的首尾節點,其中head節點存放鏈表第一個item爲null的節點,tail則並不是總指向最後一個節點。Node節點內部則維護一個變量item用來存放節點的值,next用來存放下一個節點,從而鏈接爲一個單向無界列表