Queue接口繼承了Collection接口,其內部定義了六個方法,分爲三大類,分別是新增元素、刪除元素、檢索元素。每一個大類都提供了兩個方法,這兩個方法的作用可以用下面的表格來描述:
(1).add()和offer():這兩個方法是向隊列添加元素,不同之處在於如果隊列已經添加滿了還繼續添加add()方法就會拋出異常,而offer()會返回false值(如果隊列沒有滿,則offer()添加成功後會返回true)。
(2).remove()和poll():這兩個方法都是返回隊列頂元素並且從隊列中刪除。不同的是,如果在刪除時隊列裏沒有元素了那麼remove則會拋異常,而poll則會返回null。
(3).element()和peek():這兩個方法都是取隊列頂部元素但是不刪除,和remove()、poll()有所區別。當隊列沒有元素時element()會拋異常,而peek()會返回null。
一、雙向隊列Deque
雙向隊列可以雙向操作,比如添加元素可以從兩頭添加、刪除和獲取都可以從收尾刪除獲取,這些操作都對應於相應的last和first方法。Deque也是一個雙向隊列接口,其下的實現由ArrayDeque(它的大小沒有限制,也可以手動限制)。
下面我們看雙向隊列的實現原理。我們以ArrayDeque類分析。
其實如果在創建ArrayDeque對象時不指定其初始大小它默認的就是16,如果在使用的時候發現不夠就會拓展。雙向隊列的數據存儲實際上時用數組來存儲的,在ArrayDeque中有一個全局的E[] elements,數組,之後所有的增刪改查都與此數據息息相關,此外還有兩個比較重要的變量就是head和tail變量,分別表示隊列首部和尾部。當我們從隊列的頭部取值時head值會自動+1,且會將elements[head]元素置爲null(如果是remove方法或poll方法),如果是從隊列的尾部取值,則tail的值會自動-1.其實現原理如下:
public E pollFirst() {
int h = head;
E result = elements[h]; // Element is null if deque empty
if (result == null)
return null;
elements[h] = null; // Must null out slot
head = (h + 1) & (elements.length - 1);
return result;
}
public E pollLast() {
int t = (tail - 1) & (elements.length - 1);
E result = elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}
雖然上面給出的只是pollFirst()和poolLast()方法,但是實際上poll()也是調用的pollFIrst()方法。二、阻塞隊列BlockingQueue
先來總結下BlockingQueue的特性:
1、阻塞隊列在我們處理生產者-消費者模式的時候是很有用的,尤其是如果有大量網絡請求的應用場景。當隊列裏的元素爲空時,他就等處於等待狀態,等待隊列裏存儲進新的任務或元素。
2、BlockingQueue的四種類型的方法,每一種類型都有不一樣的操作,有時候可以根據不同的場景去使用。這四種不同的類型分別是:拋出異常、返回一個特定的值(null、false、0等)、無限期的阻塞當前線程直到操作成功、在放棄任務之前等待的時間。
3、BlockingQueue不接受null元素。
4、BlockingQueue必須有一個容量限制。
5、BlockingQueue是線程安全的,其所有的方法都有內部的Lock保證安全。
6、BlockingQueue沒有任何類似“close”、“shutdown”等操作去指示不能添加更多的元素。
如下便是四種不同類型的方法,和普通的Queue就多了個阻塞機制:
Throws exception | Special value | Blocks | Times out | |
Insert | add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
Remove | remove() |
poll() |
take() |
poll(time, unit) |
Examine | element() |
peek() |
not applicable | not applicable |
方法的用於上述圖標已經很明瞭,下面我們看一個典型的生產者和消費者的例子:
(1)生產者
/**
* 生產者
*
* @author Administrator
*
*/
public static class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
// 生產對象/任務,
queue.put(produce());
}
} catch (InterruptedException ex) {
}
}
Object produce() {
cout++;
return "第" + cout + "個任務";
}
}
(2)消費者
/**
* 消費者
*
* @author Administrator
*
*/
public static class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
// 從隊列取出元素/任務,如果沒有任務就會一直處於等待狀態
consume(queue.take());
}
} catch (InterruptedException ex) {
}
}
// 實際的消費行爲
void consume(Object x) {
System.out.println(x);
}
}
最後開啓任務:
public static int cout = 1;
public static void main(String[] args) {
BlockingQueue queue = new ArrayBlockingQueue(10);
Producer pro = new Producer(queue);
Consumer con = new Consumer(queue);
// 開始生產
new Thread(pro).start();
// 開始消費
new Thread(con).start();
}
實際上,在很多場景都有這種生產者-消費者模式,比如在Volley中其就使用到了BlockingQueue來接收請求,然後獲取任務從而發起請求。(注:生產者-消費者模式很經典,在操作系統中PV鎖控制的中斷也可以實現類似生產者-消費者模式)。
常用的阻塞隊列
其實從上面的代碼可能已經猜出來了,BlockingQueue只不過是一個接口,其下有衆多的不同實現類,下面就簡單介紹下其實現類以及使用的場景:
ArrayBlockingQueue
一個由數組支持的有限阻塞隊列(其大小在創建的時候就被確定,並且不能在修改)。此隊列裏存儲的元素順序是FIFO(first-in-first-out),是一個很標準的普通隊列,也是我們最常使用到的阻塞隊列。其頭部的元素是在隊列帶的時間最長,尾部元素在隊列中呆的時間最短,新來的元素是插在尾部的,而當隊列獲取元素時是從頭部獲取的。如果試圖將一個元素put到一個full狀態的隊列,這個操作就會被阻塞,直到隊列有空位置。如果從一個empty隊列獲取新的元素同樣也會被阻塞,知道有元素可獲取。
此類爲等待的消費者/生產者提供了一個可選的公平的處理策略。此類默認的不保證阻塞的順序。可以看到ArrayBlockingQueue有三個構造器,第一個就是隻提供一個容量,第二個構造器的第一個參數就是容量,第二個參數就是是否公平加入/移除隊列,如果設置爲true就會按照FIFO的順序來入隊列和出隊列,如果爲false就不保證順序了。一個參數的構造器默認的是false。
DelayQueue
此隊列是一個無界限的隊列,只有當他的元素在隊列中超過規定時間了他纔可以被取出來,其頭部是超期時間最長的元素,尾部就是超期最短(或還未過期)的元素。如果還沒有過期的元素,那麼就不存在頭部了,將直接返當回null,他的隊列調用了元素的getDelay()方法返回的值小於或等於0就表示過期了。
這個隊列不是所有的元素都可以放進去的,必須是實現了Delayed接口的類對象纔可以放進去,否則就會報類型轉換異常,那麼什麼是Delayed接口呢?它很簡單,直接去看其API吧!我們暫時只需要知道如何使用就行!下面給出一個場景用DelayQueue實現:
作爲上班族是不能遲到早退的,不然扣錢扣得那叫一個慘不忍睹....親身體驗,我有一次就年會那天直接去年會現場了,沒有來打卡就扣了好幾百啊有木有,就半天時間啊有木有,說多了都是淚...言歸正傳。有一天Boss讓小A去加個班,並讓小A不管工作有沒有做完必須在晚上24點才能下班(我們先假設這個工作加班到死也一下子做不完),在小班後小A還得打卡驗證。那麼程序如何模擬呢?下面上代碼:
(1)老闆類
/**
* 老闆
*
* @author Administrator
*
*/
public static class Boss implements Runnable {
private final BlockingQueue queue;
Boss(BlockingQueue q) {
queue = q;
}
public void run() {
try {
// 告訴小A去加班啊,別偷懶,下班記得在24點打卡啊
queue.put(produce());
// }
} catch (InterruptedException ex) {
}
}
private Object produce() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
// 這裏指定啥時候下班:老闆讓他在24點下班
Date date = sdf.parse("2089-06-18 24:00:00");
// 構造器傳遞一個long類型的數值
return new DelayObject(date.getTime());
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
}
(2)小A
/**
* 存放到DelayQueue的元素必須實現Delay接口
*
* @author Administrator
*
*/
private static class 小A implements Delayed {
static int count = 1;
static long delay = 1;
public 小A(long delay) {
this.delay = delay;
count++;
}
public int compareTo(Delayed o) {
return 0;
}
/**
* 隊列會不停調用此方法(比如take()方法裏有個死循環,會不斷調用getDelay())判斷該元素是否可以取出來,
* 因此此參數不應該是一個固定的,應該和系統時間關聯,即隨着時間的推移 可以使得這個元素可以使用
* 當這個返回值小於等於0的時候就可以從隊列中取出這個元素了
*/
public long getDelay(TimeUnit unit) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 每次調用getDelay()時都重新計時
Date date = new Date();
String sd = sdf.format(date);
try {
Date d2 = sdf.parse(sd); // 前的時間
long diff = delay - d2.getTime(); // 兩時間差,精確到毫秒
// 此diff會1秒秒的減小,當其爲0的時候就不再調用了,直接就可以出隊列了。
return diff;
} catch (ParseException e) {
e.printStackTrace();
}
return 0;
}
/**
* 下班了
*/
public void free() {
System.out.println("哈哈哈哈哈哈---我下班了");
}
}
(3)考勤系統
/**
* 公司的考勤系統
*
* @author Administrator
*
*/
public static class SystemWorker implements Runnable {
private final BlockingQueue queue;
SystemWorker(BlockingQueue q) {
queue = q;
}
public void run() {
try {
while (true) {
// 只要還沒到下班時間,他就一直處於阻塞狀態,等待
offWork(queue.take());
}
} catch (InterruptedException ex) {
}
}
// 下班打卡:其參數就是具體的某個員工下班
void offWork(Object x) {
小A delay = (小A) x;
// 如果能下班就說
delay.free();
}
}
(4)正式運作
BlockingQueue queue = new DelayQueue();
Boss pro = new Boss(queue);
SystemWorker con = new SystemWorker(queue);
// 老闆發話
new Thread(pro).start();
// 系統開始工作
new Thread(con).start();
以上就是三個角色完成的上述場景,注意啊,小A這個類的命名我用了箇中文哈(逼死強迫症 T_T )。好,如上就是DelayQueue的工作場景和使用方法了,這裏總結下,首先主要就是確定未來時間點,也就是任務結束的時間,並且在getDelay()方法中動態的獲取當前時間進行計算,不斷地縮小差值,也就是說DelayQueue需要我們手動計算延時操作,這樣我們可以在遇到突發狀況時可以隨時更改此時間差值以提前完成任務。LinkedBlockingQueue
是一個基於連接節點的任意大小容量的隊列,這個隊列的順序是FIFO,其頭部是存入隊列最早的元素,尾部是存入隊列最晚的元素,每次都是講元素插入尾部,從頭部取出元素。LinkedBlockingQueue:比ArrayBlockingQueue有更大的吞吐量,但是在併發的情況下其性能是不可預測的。
好吧!暫時就介紹以上三種常見常用的隊列,另外還有LinkedTransferQueue、PriorityBlockingQueue、SynchronousQueue三種大家可以自行查看API使用。