Java優先順序佇列DelayedWorkQueue原理分析
我們知道執行緒池執行時,會不斷從任務佇列中獲取任務,然後執行任務。如果我們想實現延時或者定時執行任務,重要一點就是任務佇列會根據任務延時時間的不同進行排序,延時時間越短地就排在佇列的前面,先被獲取執行。
佇列是先進先出的資料結構,就是先進入佇列的資料,先被獲取。但是有一種特殊的佇列叫做優先順序佇列,它會對插入的資料進行優先順序排序,保證優先順序越高的資料首先被獲取,與資料的插入順序無關。
實現優先順序佇列高效常用的一種方式就是使用堆。
一. 用堆實現優先順序佇列
在這篇文章中,我們詳細地講解了堆排序的實現。這裡我們回顧一下。
1.1 什麼是堆
它是一個完全二叉樹,即除了最後一層節點不是滿的,其他層節點都是滿的,即左右節點都有。
它不是二叉搜尋樹,即左節點的值都比父節點值小,右節點的值都不比父節點值小,這樣查詢的時候,就可以透過二分的方式,效率是(log N)。
它是特殊的二叉樹,它要求父節點的值不能小於子節點的值。這樣保證大的值在上面,小的值在下面。所以堆遍歷和查詢都是低效的,因為我們只知道
從根節點到子葉節點的每條路徑都是降序的,但是各個路徑之間都是沒有聯絡的,查詢一個值時,你不知道應該從左節點查詢還是從右節點開始查詢。它可以實現快速的插入和刪除,效率都在(log N)左右。所以它可以實現優先順序佇列。
堆是一個二叉樹,但是它最簡單的方式是透過陣列去實現二叉樹,而且因為堆是一個完全二叉樹,就不存在陣列空間的浪費。怎麼使用陣列來儲存二叉樹呢?
就是用陣列的下標來模擬二叉樹的各個節點,比如說根節點就是0,第一層的左節點是1,右節點是2。由此我們可以得出下列公式:
// 對於n位置的節點來說:int left = 2 * n + 1; // 左子節點int right = 2 * n + 2; // 右子節點int parent = (n - 1) / 2; // 父節點,當然n要大於0,根節點是沒有父節點的
對於堆來說,只有兩個操作,插入insert和刪除remove,不管插入還是刪除保證堆的成立條件,1.是完全二叉樹,2.父節點的值不能小於子節點的值。
public void insert(int value) { // 第一步將插入的值,直接放在最後一個位置。並將長度加一 store[size++] = value; // 得到新插入值所在位置。 int index = size - 1; while(index > 0) { // 它的父節點位置座標 int parentIndex = (index - 1) / 2; // 如果父節點的值小於子節點的值,你不滿足堆的條件,那麼就交換值 if (store[index] > store[parentIndex]) { swap(store, index, parentIndex); index = parentIndex; } else { // 否則表示這條路徑上的值已經滿足降序,跳出迴圈 break; } } }
主要步驟:
直接將value插入到size位置,並將size自增,這樣store陣列中插入一個值了。
要保證從這個葉節點到根節點這條路徑上的節點,滿足父節點的值不能小於子節點。
透過int parentIndex = (index - 1) / 2得到父節點,如果比父節點值大,那麼兩者位置的值交換,然後再拿這個父節點和它的父父節點比較。
直到這個節點值比父節點值小,或者這個節點已經是根節點就退出迴圈。
因為我們每次只插入一個值,所以只需要保證新插入位置的葉節點到根節點路徑滿足堆的條件,因為其他路徑沒做操作,肯定是滿足條件的。第二因為是直接在size位置插入值,所以肯定滿足是完全二叉樹這個條件。因為每次迴圈index都是除以2這種倍數遞減的方式,所以它最多迴圈次數是(log N)次。
public int remove() { // 將根的值記錄,最後返回 int result = store[0]; // 將最後位置的值放到根節點位置 store[0] = store[--size]; int index = 0; // 透過迴圈,保證父節點的值不能小於子節點。 while(true) { int leftIndex = 2 * index + 1; // 左子節點 int rightIndex = 2 * index + 2; // 右子節點 // leftIndex >= size 表示這個子節點還沒有值。 if (leftIndex >= size) break; int maxIndex = leftIndex; if (store[leftIndex]在堆中最大值就在根節點,所以操作步驟:
將根節點的值儲存到result中。
將最後節點的值移動到根節點,再將長度減一,這樣滿足堆成立第一個條件,堆是一個完全二叉樹。
使用迴圈,來滿足堆成立的第二個條件,父節點的值不能小於子節點的值。
最後返回result。
那麼怎麼樣滿足堆的第二個條件呢?
因為根點的值現在是新值,那麼就有可能比它的子節點小,所以就有可能要進行交換。
我們要找出左子節點和右子節點那個值更大,因為這個值可能要和父節點值進行交換,如果它不是較大值的話,它和父節點進行交換之後,就會出現父節點的值小於子節點。
將找到的較大子節點值和父節點值進行比較。
如果父節點的值小於它,那麼將父節點和較大子節點值進行交換,然後再比較較大子節點和它的子節點。
如果父節點的值不小於子節點較大值,或者沒有子節點(即這個節點已經是葉節點了),就跳出迴圈。
每次迴圈我們都是以2的倍數遞增,所以它也是最多迴圈次數是(log N)次。
所以透過堆這種方式可以快速實現優先順序佇列,它的插入和刪除操作的效率都是O(log N)。
二. DelayedWorkQueue類
static class DelayedWorkQueue extends AbstractQueueimplements BlockingQueue { 從定義中看出DelayedWorkQueue是一個阻塞佇列。
2.1 重要成員屬性
// 初始時,陣列長度大小。 private static final int INITIAL_CAPACITY = 16; // 使用陣列來儲存佇列中的元素。 private RunnableScheduledFuture>[] queue = new RunnableScheduledFuture>[INITIAL_CAPACITY]; // 使用lock來保證多執行緒併發安全問題。 private final ReentrantLock lock = new ReentrantLock(); // 佇列中儲存元素的大小 private int size = 0; //特指佇列頭任務所線上程 private Thread leader = null; // 當佇列頭的任務延時時間到了,或者有新的任務變成佇列頭時,用來喚醒等待執行緒 private final Condition available = lock.newCondition();DelayedWorkQueue是用陣列來儲存佇列中的元素,那麼我們看看它是怎麼實現優先順序佇列的。
2.2 插入元素排序siftUp方法
private void siftUp(int k, RunnableScheduledFuture> key) { // 當k==0時,就到了堆二叉樹的根節點了,跳出迴圈 while (k > 0) { // 父節點位置座標, 相當於(k - 1) / 2 int parent = (k - 1) >>> 1; // 獲取父節點位置元素 RunnableScheduledFuture> e = queue[parent]; // 如果key元素大於父節點位置元素,滿足條件,那麼跳出迴圈 // 因為是從小到大排序的。 if (key.compareTo(e) >= 0) break; // 否則就將父節點元素存放到k位置 queue[k] = e; // 這個只有當元素是ScheduledFutureTask物件例項才有用,用來快速取消任務。 setIndex(e, k); // 重新賦值k,尋找元素key應該插入到堆二叉樹的那個節點 k = parent; } // 迴圈結束,k就是元素key應該插入的節點位置 queue[k] = key; setIndex(key, k); }透過迴圈,來查詢元素key應該插入在堆二叉樹那個節點位置,並互動父節點的位置。具體流程在前面已經介紹過了。
2.3 移除元素排序siftDown方法
private void siftDown(int k, RunnableScheduledFuture> key) { int half = size >>> 1; // 透過迴圈,保證父節點的值不能小於子節點。 while (k c = queue[child]; // 右子節點, 相當於 (k * 2) + 2 int right = child + 1; // 如果左子節點元素值大於右子節點元素值,那麼右子節點才是較小值的子節點。 // 就要將c與child值重新賦值 if (right 0) c = queue[child = right]; // 如果父節點元素值小於較小的子節點元素值,那麼就跳出迴圈 if (key.compareTo(c)透過迴圈,保證父節點的值不能小於子節點。
2.4 插入元素方法
public void put(Runnable e) { offer(e); } public boolean add(Runnable e) { return offer(e); } public boolean offer(Runnable e, long timeout, TimeUnit unit) { return offer(e); }我們發現與普通阻塞佇列相比,這三個新增方法都是呼叫offer方法。那是因為它沒有佇列已滿的條件,也就是說可以不斷地向DelayedWorkQueue新增元素,當元素個數超過陣列長度時,會進行陣列擴容。
public boolean offer(Runnable x) { if (x == null) throw new NullPointerException(); RunnableScheduledFuture> e = (RunnableScheduledFuture>)x; // 使用lock保證併發操作安全 final ReentrantLock lock = this.lock; lock.lock(); try { int i = size; // 如果要超過陣列長度,就要進行陣列擴容 if (i >= queue.length) // 陣列擴容 grow(); // 將佇列中元素個數加一 size = i + 1; // 如果是第一個元素,那麼就不需要排序,直接賦值就行了 if (i == 0) { queue[0] = e; setIndex(e, 0); } else { // 呼叫siftUp方法,使插入的元素變得有序。 siftUp(i, e); } // 表示新插入的元素是佇列頭,更換了佇列頭, // 那麼就要喚醒正在等待獲取任務的執行緒。 if (queue[0] == e) { leader = null; // 喚醒正在等待等待獲取任務的執行緒 available.signal(); } } finally { lock.unlock(); } return true; }主要是三步:
元素個數超過陣列長度,就會呼叫grow()方法,進行陣列擴容。
將新元素e新增到優先順序佇列中對應的位置,透過siftUp方法,保證按照元素的優先順序排序。
如果新插入的元素是佇列頭,即更換了佇列頭,那麼就要喚醒正在等待獲取任務的執行緒。這些執行緒可能是因為原佇列頭元素的延時時間沒到,而等待的。
陣列擴容方法:
private void grow() { int oldCapacity = queue.length; // 每次擴容增加原來陣列的一半數量。 int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50% if (newCapacity2.5 獲取佇列頭元素
2.5.1 立即獲取佇列頭元素
public RunnableScheduledFuture> poll() { final ReentrantLock lock = this.lock; lock.lock(); try { RunnableScheduledFuture> first = queue[0]; // 佇列頭任務是null,或者任務延時時間沒有到,都返回null if (first == null || first.getDelay(NANOSECONDS) > 0) return null; else // 移除佇列頭元素 return finishPoll(first); } finally { lock.unlock(); } }當佇列頭任務是null,或者任務延時時間沒有到,表示這個任務還不能返回,因此直接返回null。否則呼叫finishPoll方法,移除佇列頭元素並返回。
// 移除佇列頭元素 private RunnableScheduledFuture> finishPoll(RunnableScheduledFuture> f) { // 將佇列中元素個數減一 int s = --size; // 獲取佇列末尾元素x RunnableScheduledFuture> x = queue[s]; // 原佇列末尾元素設定為null queue[s] = null; if (s != 0) // 因為移除了佇列頭元素,所以進行重新排序。 siftDown(0, x); setIndex(f, -1); return f; }這個方法與我們在第一節中,介紹堆的刪除方法一樣。
先將佇列中元素個數減一。
將原佇列末尾元素設定成佇列頭元素,再將佇列末尾元素設定為null。
呼叫siftDown(0, x)方法,保證按照元素的優先順序排序。
2.5.2 等待獲取佇列頭元素
public RunnableScheduledFuture> take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { RunnableScheduledFuture> first = queue[0]; // 如果沒有任務,就讓執行緒在available條件下等待。 if (first == null) available.await(); else { // 獲取任務的剩餘延時時間 long delay = first.getDelay(NANOSECONDS); // 如果延時時間到了,就返回這個任務,用來執行。 if (delay如果佇列中沒有任務,那麼就讓當前執行緒在available條件下等待。如果佇列頭任務的剩餘延時時間delay大於0,那麼就讓當前執行緒在available條件下等待delay時間。
如果佇列插入了新的佇列頭,它的剩餘延時時間肯定小於原來佇列頭的時間,這個時候就要喚醒等待執行緒,看看它是否能獲取任務。
2.5.3 超時等待獲取佇列頭元素
public RunnableScheduledFuture> poll(long timeout, TimeUnit unit) throws InterruptedException { long nanos = unit.toNanos(timeout); final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { RunnableScheduledFuture> first = queue[0]; // 如果沒有任務。 if (first == null) { // 超時時間已到,那麼就直接返回null if (nanos與take方法相比較,就要考慮設定的超時時間,如果超時時間到了,還沒有獲取到有用任務,那麼就返回null。其他的與take方法中邏輯一樣。
三. 總結
使用優先順序佇列DelayedWorkQueue,保證新增到佇列中的任務,會按照任務的延時時間進行排序,延時時間少的任務首先被獲取。
作者:wo883721
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2144/viewspace-2802428/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 封裝優先順序佇列封裝佇列
- 佇列 優先順序佇列 python 程式碼實現佇列Python
- 棧,佇列,優先順序佇列簡單介面使用佇列
- Facebook的分散式優先順序佇列FOQS分散式佇列
- Python3 執行緒優先順序佇列( Queue)Python執行緒佇列
- Python 列表推導及優先順序佇列的實現Python佇列
- 個推基於 Apache Pulsar 的優先順序佇列方案Apache佇列
- RMQ——支援合併和優先順序的訊息佇列MQ佇列
- java運算子優先順序Java
- java setPriority()設定優先順序Java
- 基於EasyNetQ封裝RabbitMQ,優先順序郵件服務佇列封裝MQ佇列
- 原始碼解析C#中PriorityQueue(優先順序佇列)的實現原始碼C#佇列
- CSS優先順序CSS
- 佇列-順序儲存佇列
- python運算子及優先順序順序Python
- Java之執行緒的優先順序Java執行緒
- 【資料結構】佇列(順序佇列、鏈佇列)的JAVA程式碼實現資料結構佇列Java
- Android程式優先順序Android
- 中斷優先順序
- Yacc使用優先順序
- Python例項屬性的優先順序分析Python
- PHP優先佇列PHP佇列
- java字串連線和運算子優先順序Java字串
- SpringBoot配置檔案優先順序載入順序Spring Boot
- 運算子的優先順序
- SQL 優先順序join>whereSQL
- .NET 6 優先佇列 PriorityQueue 實現分析佇列
- Android程式設計師會遇到的演算法(part 6 優先順序佇列PriorityQueue)Android程式設計師演算法佇列
- 佇列的順序儲存--迴圈佇列的建立佇列
- 面向大規模佇列,百萬併發的多優先順序消費系統設計佇列
- STL 優先佇列 用法佇列
- 淺談優先佇列佇列
- 堆與優先佇列佇列
- [譯]HTTP/2的優先順序HTTP
- css 選擇器優先順序CSS
- Yarn任務優先順序配置Yarn
- ansible 變數優先順序示例變數
- C++運算子優先順序C++