1.Queue用於模擬隊列這種數據結構,隊列一般是指「先進先出」(FIFO)的容器。新元素插入(offer)到隊列的尾部,訪問元素(poll)操做會返回隊列頭部的元素。一般,隊列不容許隨機訪問隊列中的元素。這種結構就如同咱們生活中的排隊同樣。java
2.Queue家族有兩大分支,即阻塞隊列和非阻塞隊列,阻塞隊列都是用於併發編程當中。編程
阻塞隊列會對當前線程產生阻塞,好比一個線程從一個空的阻塞隊列中取元素,此時線程會被阻塞直到阻塞隊列中有了元素。當隊列中有元素後,被阻塞的線程會自動被喚醒(不須要咱們編寫代碼去喚醒),或者針對有界隊列不光是獲取元素時會阻塞,當隊列中沒有空餘空間的時候相對列插入元素也會產生阻塞。數組
非阻塞隊列在插入元素和獲取元素時不會對當前線程產生阻塞,在不涉及併發環境以及併發環境中不涉及多線程共享變量的代碼中常用。在併發環境中使用非阻塞隊列的時候有一個很大問題就是:它不會對當前線程產生阻塞,那麼在面對相似消費者-生產者的模型時,就必須額外地實現同步策略以及線程間喚醒策略,這個實現起來就很是麻煩。安全
非阻塞隊列數據結構
1.PriorityQueue保存隊列元素的順序不是按加入隊列的順序,而是按隊列元素的大小進行從新排序。所以當調用peek()或pool()方法取出隊列中頭部的元素時,並非取出最早進入隊列的元素,而是取出隊列中的最小的元素。PriorityQueue中的元素能夠默認天然排序(也就是數字默認是小的在隊列頭,字符串則按字典序排列)或者經過提供的Comparator(比較器)在隊列實例化時指定的排序方式。當PriorityQueue中沒有指定Comparator時,加入PriorityQueue的元素必須實現了Comparable接口(即元素是可比較的),不然會致使 ClassCastException。多線程
注:任何集合中Comparator的優先級要高於實現Comparable接口。併發
2.PriorityQueue 本質也是一個動態數組,在這一方面與ArrayList是一致的。函數
PriorityQueue調用默認的構造方法時,使用默認的初始容量(DEFAULT_INITIAL_CAPACITY=11)建立一個 PriorityQueue,並根據其天然順序來排序其元素(使用加入其中的集合元素實現的Comparable)。spa
從下面的構造方法能夠看出,內部維護了一個動態數組。線程
當添加元素到集合時,會先檢查數組是否還有餘量,有餘量則把新元素加入集合,沒餘量則調用grow()方法增長容量,而後調用siftUp將新加入的元素排序插入對應位置。
grow()方法在隊列容量小於64時,每次增長一倍+2,當容量大於64時,每次擴容50%
除此以外,還要注意:
①PriorityQueue不是線程安全的。若是多個線程中的任意線程從結構上修改了列表, 則這些線程不該同時訪問 PriorityQueue 實例,這時請使用線程安全的PriorityBlockingQueue 類。
②不容許插入 null 元素。
③PriorityQueue實現插入方法(offer、poll、remove() 和 add 方法) 的時間複雜度是O(log(n)) ;實現 remove(Object) 和 contains(Object) 方法的時間複雜度是O(n) ;實現檢索方法(peek、element 和 size)的時間複雜度是O(1)。因此在遍歷時,若不須要刪除元素,則以peek的方式遍歷每一個元素。
④方法iterator()中提供的迭代器並不保證以有序的方式遍歷優PriorityQueue中的元素。而是通過比較排序後的順序遍歷元素。
3.Deque接口表明一個"雙端隊列",雙端隊列能夠同時從兩端來添加、刪除元素,所以Deque的實現類既能夠當成隊列使用、也能夠當成棧使用。LinkedList也實現了Deque接口,因此也能夠被看成雙端隊列使用。
當 Deque 當作 Queue隊列使用時(FIFO),添加元素是添加到隊尾,刪除時刪除的是頭部元素。
Deque 也能當Stack棧用(LIFO)。這時入棧、出棧元素都是在 雙端隊列的頭部 進行。
注意:Stack過於古老,而且實現地很是很差,所以如今基本已經不用了,能夠直接用Deque來代替Stack進行棧操做。
4.ArrayDeque顧名思義,就是用數組實現的Deque;既然是底層是數組那確定也能夠指定其容量,也能夠不指定,默認長度是16,而後根據添加的元素的個數,動態擴展。ArrayDeque因爲是兩端隊列,因此其順序是按照元素插入數組中對應位置產生的(下面會具體說明)。
因爲自己數據結構的限制,ArrayDeque沒有像ArrayList中的trimToSize方法能夠爲本身瘦身。ArrayDeque的使用方法就是上面的Deque的使用方法,基本沒有對Deque拓展什麼方法。
5.ArrayDeque爲了知足能夠同時在數組兩端插入或刪除元素的需求,其內部的動態數組還必須是循環的,即循環數組(circular array),也就是說數組的任何一點均可能被看做起點或者終點。
ArrayDeque維護了兩個變量,表示ArrayDeque的頭和尾。
當向頭部插入元素時,head下標減一而後插入元素。而 tail表示的索引爲當前末尾元素表示的索引值加一。若當向尾部插入元素時,直接向tail表示的位置插入,而後tail再減一。
下面具體看看ArrayDeque怎麼把循環數組實際應用的?
咱們從源碼當中獲取答案
從隊列頭部添加元素
從隊列尾部添加元素
當加入元素時,先看是否爲空(ArrayDeque不能夠存取null元素,由於系統根據某個位置是否爲null來判斷元素的存在)。而後head-1插入元素。head = (head - 1) & (elements.length - 1)很好的解決了下標越界的問題。這段代碼至關於取模,同時解決了head爲負值的狀況。由於elements.length必需是2的指數倍(代碼中有具體操做),elements - 1就是二進制低位全1,跟head - 1相與以後就起到了取模的做用。若是head - 1爲負數,其實只多是-1,當爲-1時,和elements.length - 1進行與操做,這時結果爲elements.length - 1。其餘狀況則不變,等於它自己。
當插入元素後,在進行判斷是否還有餘量。由於tail老是指向下一個可插入的空位,也就意味着elements數組至少有一個空位,因此插入元素的時候不用考慮空間問題。
下面再說說擴容函數doubleCapacity(),其邏輯是申請一個更大的數組(原數組的兩倍),而後將原數組複製過去。
再說說擴容時複製的機制,過程以下圖所示:
圖中咱們看到,複製分兩次進行,第一次複製head右邊的元素,第二次複製head左邊的元素。代碼以下:
6.再來詳細說說ArrayDeque中的絕妙之筆,就是那一行代碼將環形的數組的各類實現問題所有解決:
.下標越界的問題
.head標記爲負數的問題
上面的兩個問題咱們徹底能夠經過代碼來去解決,可是你先想一想你須要多少行代碼。而Java開發者們只須要這一行:
head = (head - 1) & (elements.length - 1)/tail = (tail + 1) & (elements.length - 1)
上面兩行代碼咱們在ArrayDeque的從隊列頭部添加元素的方法以及從隊列尾部添加元素的方法中能夠看到,他們分別是計算元素從頭插入和從尾插入的下標位置的,而且很是巧妙的解決了上面兩個問題。
咱們再來回顧下ArrayDeque的插入邏輯,ArrayDeque中維繫了一個數組來充當隊列,而且經過head和tail分別記錄了頭結點下標和尾節點下標。當從頭部插入時,head減一。當從尾部插入時tail加一。
針對ArrayDeque中的數組,如何作到環形訪問?就是經過head和tail來實現的。初始head和tail爲0。當head小於0時,head此時應該指向數組的最後一個下標。一樣原理當tail大於數組最後的下標時,tail應當指向數組第一個下標。當head和tail再次相等時,則表示隊列已經滿了,須要擴容。
咱們來分析下上面的代碼以head = (head - 1) & (elements.length - 1) 爲例,咱們來分析下它都作了什麼:
(1) 當咱們第一次從隊列的頭部添加元素時,此時head爲0,addFirst方法執行上面代碼的前半部分(head - 1) = -1 後半部分 (elements.length - 1) 爲 16 -1 = 15(16爲ArrayDeque默認初始化容量)
(2) 當 -1 & 15 時,神奇的事情發生了,-1 & 15的結果爲15。也就是說,當第一次向數組頭部添加元素時,head會小於0,此時上面的代碼將 -1 這個越界的下標幫咱們優雅的轉化爲了數組的末尾下標15。這中間具體發生了什麼?
. -1的二進制表示是須要先找到的1二進制表示,也就是原碼,再計算其反碼,反碼就是1和0互換 補碼就是在反碼的基礎上最右邊一位加1 根據二進制的進位原則的補碼 就是-1的二進制表示
0000 0001 //1的源碼
1111 1110 //1的反碼
1111 1111 //1 的補碼 也就是-1
&
1111 1111 //15二進制表示
————————————
1111 1111 //結果爲15
. 爲何說 & 運算至關於取模呢?
位運算(&)效率要比取模運算(%)高不少,主要緣由是位運算直接對內存數據進行操做,不須要轉成十進制,所以處理速度很是快。
a % b == a & (b - 1)
前提:b 爲 2^N
具體的效率對比這裏不贅述,簡單說一下爲何 & 能夠代替 % :
X % 2^n = X & (2^n - 1)
2^n 表示 2 的 n 次方,也就是說, 一個數對 2^n 取模至關於一個數和 (2^n - 1) 作按位與運算。
假設 n 爲 3,則 2^3 = 8,表示成 2 進制就是 1000。2^3 - 1 = 7 ,即 0111。
此時 X & (2^3 - 1) 就至關於取 X 的 2 進制的最後三位數。
從 2 進制角度來看,X / 8 至關於 X >> 3,即把 X 右移 3 位,此時獲得了 X / 8 的商,而被移掉的部分(後三位),則是 X % 8,也就是餘數。
推廣到通常:
對於全部 2^n 的數,二進制表示爲:
1000…000,1 後面跟 n 個 0
而 2^n - 1 的二進制爲:
0111…111,0 後面跟 n 個 1
X / 2^n 是 X >> n,那麼 X & (2^n - 1) 就是取被移掉的後 n 位,也就是 X % 2^n。
而tail的計算原理同上面的相同,這裏就不作闡述了。
7.再來講說ArrayDeque的初始化,確定有人會想,爲何默認初始化長度會是16?其實,重點不是16,咱們從上面的針對每次添加元素計算head的方法中看到了head = (head - 1) & (elements.length - 1),而且也分析了爲何&操做至關於取模,緣由就是X % 2^n = X & (2^n - 1)這個公式,因爲這個公式取模的高效,所以初始化預計擴容時ArrayDeque的廠區都必須是2的整數倍。咱們來看下ArrayDeque的初始化方法:
除了默認初始化,另外兩個都有一個allocateElements方法,他的做用是尋找比輸入的廠區大的最近2次冪,由於用戶不必定會傳入2的次冪的整數。
看到這段迷之代碼了嗎?在HashMap中也有一段相似的實現。但要讀懂它,咱們須要先掌握如下幾個概念:
在java中,int的長度是32位,有符號int能夠表示的值範圍是 (-2)31 到 231-1,其中最高位是符號位,0表示正數,1表示負數。
>>>:無符號右移,忽略符號位,空位都以0補齊。
|:位或運算,按位進行或操做,逢1爲1。
咱們知道,計算機存儲任何數據都是採用二進制形式,因此一個int值爲80的數在內存中多是這樣的:
0000 0000 0000 0000 0000 0000 0101 0000
比80大的最近的2次冪是128,其值是這樣的:
0000 0000 0000 0000 0000 0000 1000 0000
咱們多找幾組數據就能夠發現規律:
每一個2的次冪用二進制表示時,只有一位爲 1,其他位均爲 0(不包含符合位)
要找到比一個數大的2的次冪(在正數範圍內),只須要將其最高位左移一位(從左往右第一個 1 出現的位置),其他位置 0 便可。
但從實踐上講,沒有可行的方法可以進行以上操做,即便經過&操做符能夠將某一位置 0 或置 1,也沒法確認最高位出現的位置,也就是基於最高位進行操做不可行。
但還有一個很整齊的數字能夠被咱們利用,那就是 2n-1,咱們看下128-1=127的表示形式:
0000 0000 0000 0000 0000 0000 0111 1111
把它和80對比一下:
0000 0000 0000 0000 0000 0000 0101 0000 //80
0000 0000 0000 0000 0000 0000 0111 1111 //127
能夠發現,咱們只要把80從最高位起每一位全置爲1,就能夠獲得離它最近且比它大的 2n-1,最後再執行一次+1操做便可。具體操做步驟爲(爲了演示,這裏使用了很大的數字):
原值:
0011 0000 0000 0000 0000 0000 0000 0010
無符號右移1位
0001 1000 0000 0000 0000 0000 0000 0001
與原值|操做:
0011 1000 0000 0000 0000 0000 0000 0011
能夠看到最高2位都是1了,也僅能保證前兩位爲1,這時就能夠直接移動兩位
無符號右移2位
0000 1110 0000 0000 0000 0000 0000 0000
與原值|操做:
0011 1110 0000 0000 0000 0000 0000 0011
此時就能夠保證前4位爲1了,下一步移動4位
無符號右移4位
0000 0011 1110 0000 0000 0000 0000 0000
與原值|操做:
0011 1111 1110 0000 0000 0000 0000 0011
此時就能夠保證前8位爲1了,下一步移動8位
無符號右移8位
0000 0000 0011 1111 1110 0000 0000 0000
與原值|操做:
0011 1111 1111 1111 1110 0000 0000 0011
此時前16位都是1,只須要再移位操做一次,便可把32位都置爲1了。
無符號右移16位
0000 0000 0000 0000 0011 1111 1111 1111
與原值|操做:
0011 1111 1111 1111 1111 1111 1111 1111
進行+1操做:
0100 0000 0000 0000 0000 0000 0000 0000
如此通過11步操做後,咱們終於找到了合適的2次冪。寫成代碼就是:
不過爲了防止溢出,致使出現負值(若是把符號位置爲1,就爲負值了)還須要一次校驗:
至此,初始化的過程就完畢了。
8.咱們常常看到說能夠基於LinkedList去實現隊列或者棧,由於LinkedList自己也是Deque雙端隊列,其實經過ArrayDeque來實現隊列或棧要比LinkedList效率高,由於LinkedList在向列表添加元素時,須要new Node對象,而ArrayDeque內部基於數組建立的,不須要new對象,所以ArrayDeque在實現隊列或棧時要有必定優點。
咱們看到50w次push和pop差距就已經很明顯了,這也體現了了解數據結構和源碼實現,可以有助於咱們寫出更加優秀的代碼。
阻塞隊列
ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。
DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
(待更新)