資料結構與演算法學習筆記之先進先出的佇列
前言
佇列是一種非常實用的資料結構,類似於生活中發排隊,可應用於生活,開發中各個方面,比如共享印表機(先請求先列印),訊息佇列。你想知道他們是怎麼工作的麼。那就來一起學習一下佇列吧
正文
一、佇列的定義?
1.一種先進先出的線性表
2.只允許入棧 push()和出棧 pop()
在後端(稱為rear)進行插入操作,在前端(稱為front)進行刪除操作。
二、如何用程式碼實現佇列?
1.java中JDK提供了Queue介面
使得LinkedList實現了該介面,所以使用佇列的時候,一般採用LinkedList。因為LinkedList是雙向連結串列,可以很方便的實現佇列的所有功能。
Queue使用時要儘量避免Collection的add()和remove()方法,而是要使用offer()來加入元素,使用poll()來獲取並移出元素。它們的優點是通過返回值可以判斷成功與否,add()和remove()方法在失敗的時候會丟擲異常。 如果要使用前端而不移出該元素,使用
element()或者peek()方法。
2.陣列實現(順序佇列)
public class ArrayQueue {
//儲存資料的陣列
private String[] items;
//記錄陣列容量
private int n;
private int size;
//head記錄隊頭索引,tail記錄隊尾索引
private int head = 0;
private int tail = 0;
//申請一個指定容量的佇列
public ArrayQueue(int capacity){
items = new String[capacity];
n = capacity;
}
/*
* 入隊:
* 1.堆滿的時,入隊失敗
* 1.1頻繁出入隊,造成陣列使用不連續
* 1.2在入隊的時候,集中觸發進行資料搬移
* 2.在末尾插入資料,注意tail指向隊尾元素的索引+1
*/
public boolean enqueue(String item){
//表示隊滿
if(head == 0 && tail == n)
return false;
//表示需要資料搬移
else if(head != 0 && tail == n){
for (int i = head; i < tail; i++) {
items[i-head] = items[i];
}
head = 0;
tail = tail - head;
}
//將資料加入佇列
items[tail++] = item;
size++;
return true;
}
//出隊:1.隊空時,出隊失敗;2.出隊,head索引+1
public String dequeue(){
String res = null;
if(head == tail) return res;
res = items[head++];
size--;
return res;
}
}
(程式碼來源於姜威)
3.連結串列實現(鏈式佇列)
public class LinkedQueue {
//定義一個節點類
private class Node{
String value;
Node next;
}
//記錄佇列元素個數
private int size = 0;
//head指向隊頭結點,tail指向隊尾節點
private Node head;
private Node tail;
//申請一個佇列
public LinkedQueue(){}
//入隊
public boolean enqueue(String item){
Node newNode = new Node();
newNode.value = item;
if (size == 0) head = newNode;
else tail.next = newNode;
tail = newNode;
size++;
return true;
}
//出隊
public String dequeue(){
String res = null;
if(size == 0) return res;
if(size == 1) tail = null;
res = head.value;
head = head.next;
size--;
return res;
}
}
(程式碼來源於姜威)
4.迴圈佇列(基於陣列)
迴圈佇列的實現
public class LoopArrayQueue {
//儲存資料的陣列
private String[] items;
//記錄陣列容量
private int n;
private int size = 0;
//head記錄隊頭索引,tail記錄隊尾索引
private int head = 0;
private int tail = 0;
//申請一個指定容量的佇列
public LoopArrayQueue(int capacity){
items = new String[capacity];
n = capacity;
}
//入隊:關鍵在於隊滿的條件
public boolean enqueue(String item){
if ((tail + 1) % n == head) return false;
items[tail] = item;
tail = (tail + 1) % n;
size++;
return true;
}
//出隊:關鍵在於隊空的條件
public String dequeue(){
String res = null;
if(head == tail) return res;
res = items[head];
head = (head + 1) % n;
size--;
return res;
}
}
(程式碼來源於姜威)
三、佇列的常見的應用阻塞佇列和併發佇列
1.阻塞佇列
1)在佇列的基礎上增加阻塞操作,形成了阻塞佇列。
2)阻塞佇列就是在佇列為空的時候,從隊頭取資料會被阻塞,因為此時還沒有資料可取,直到佇列中有了資料才能返回;如果佇列已經滿了,那麼插入資料的操作就會被阻塞,直到佇列中有空閒位置後再插入資料,然後在返回。
3)“生產者-消費者模型”
(圖片來源於王爭)
基於阻塞佇列實現的“生產者-消費者模型”可以有效地協調生產和消費的速度。
當“生產者”生產資料的速度過快,“消費者”來不及消費時,儲存資料的佇列很快就會滿了,這時生產者就阻塞等待,直到“消費者”消費了資料,“生產者”才會被喚醒繼續生產。不僅如此,基於阻塞佇列,我們還可以通過協調“生產者”和“消費者”的個數,來提高資料處理效率,比如配置幾個消費者,來應對一個生產者。
2.併發佇列
1)在多執行緒的情況下,會有多個執行緒同時操作佇列,這時就會存線上程安全問題。
執行緒安全問題的佇列就稱為併發佇列。
2)併發佇列簡單的實現就是在enqueue()、dequeue()方法上加鎖,但是鎖粒度大併發度會比較低,同一時刻僅允許一個存或取操作。
3)基於陣列的迴圈佇列利用CAS原子操作,可以實現非常高效的併發佇列。這也是迴圈佇列比鏈式佇列應用更加廣泛的原因。
3.執行緒池資源枯竭是的處理
在資源有限的場景,當沒有空閒資源時,基本上都可以通過“佇列”這種資料結構來實現請求排隊。
四、佇列線上程池等有限資源中的應用
當我們向固定大小的執行緒池中請求一個執行緒時,如果執行緒池中沒有空閒資源了,這個時候執行緒池如何處理這個請求?是拒絕請求還是排隊請求?各種處理策略又是怎麼實現的呢?
兩種處理策略:
非阻塞的處理方式,直接拒絕任務請求
阻塞的處理方式,將請求排隊,等有空閒執行緒,取出佇列中請求繼續處理
基於連結串列的實現方式,可以實現一個支援無線排隊的無界佇列,但是可能會導致過多的請求排隊,請求處理的響應時間過長
基於陣列的實現的有界佇列,佇列的大小有限,所以執行緒池中排隊的請求超過佇列大小時,接下來的請求就會被拒絕,這種方式對響應時間敏感的系統,更加合適;
佇列可以應用在任何有限的資源池中,當沒有空閒資源都可以通過“佇列”來實現請求排隊
五、思考
1.除了執行緒池這種池結構會用到佇列排隊請求,還有哪些類似執行緒池結構或者場景中會用到佇列的排隊請求呢?
在很多偏底層的系統、框架、中介軟體的開發中,起著關鍵性的作用。比如高效能佇列 Disruptor、Linux 環形快取,都用到了迴圈併發佇列;Java concurrent 併發包利用 ArrayBlockingQueue 來實現公平鎖等。
分散式訊息佇列,如 kafka 也是一種佇列
2.今天講到併發佇列,關於如何實現無鎖的併發佇列,網上有很多討論。對這個問題,你怎麼看?
可以使用 cas + 陣列的方式實現。考慮使用CAS實現無鎖佇列,則在入隊前,獲取tail位置,入隊時比較tail是否發生變化,如果否,則允許入隊,反之,本次入隊失敗。出隊則是獲取head位置,進行cas
相關文章
以上內容為個人的學習筆記,僅作為學習交流之用。
歡迎大家關注公眾號,不定時乾貨,只做有價值的輸出
版權:本文版權歸作者
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任