1. 程式人生 > >資料結構與演算法學習筆記之先進先出的佇列

資料結構與演算法學習筆記之先進先出的佇列

前言

  佇列是一種非常實用的資料結構,類似於生活中發排隊,可應用於生活,開發中各個方面,比如共享印表機(先請求先列印),訊息佇列。你想知道他們是怎麼工作的麼。那就來一起學習一下佇列吧

正文

一、佇列的定義?

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

相關文章

以上內容為個人的學習筆記,僅作為學習交流之用。

 

歡迎大家關注公眾號,不定時乾貨,只做有價值的輸出

版權:本文版權歸作者
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任