DelayQueue阻塞隊列第二章:源碼解析

DelayQueue阻塞隊列系列文章

DelayQueue阻塞隊列第一章:代碼示例
DelayQueue阻塞隊列第二章:源碼解析

介紹

DelayQueue是java併發包中提供的延遲阻塞隊列,業務場景一般是下單後多長時間過期,定時執行程序等

1-DelayQueue的組成結構

/**
 * DelayQueue隊列繼承了AbstractQueue,並且實現BlockingQueue的方法
 */
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

    //使用ReentrantLock進行線程的同步
    private final transient ReentrantLock lock = new ReentrantLock();
    //使用優先級隊列PriorityQueue作爲存放數據的隊列
    private final PriorityQueue<E> q = new PriorityQueue<E>();
  
    //使用leader/follower模式來避免多線程性能的消耗
    private Thread leader = null;

    //使用Condition等待隊列來保存請求的線程(l/f模式)
    private final Condition available = lock.newCondition();

DelayQueue中的元素需要實現Delayed接口,重寫getDelay()和compareTo()方法,其中getDelay()方法是爲了獲取隊列元素延遲剩餘時間,compareTo()方法是爲了對隊列中的元素進行一個排序,使符合條件的元素排在隊列的最前面

DelayQueue內部的實現基本就是依靠重寫BlockingQueue方法,使用ReentrantLock進行同步操作,使用PriorityQueue存放隊列元素,Condition存放訪問線程

DelayQueue內部採用了leader/follower設計模式,旨在減小多線程的消耗,本文不詳細介紹

2源碼實現細節

offer方法:將元素加入到延遲隊列中去

 public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //元素加入優先級隊列
            q.offer(e);
            //如果新加入的元素e就是隊列的頭元素,將leader置爲null切喚醒等待線程
            if (q.peek() == e) {        //Q1此處爲何要獲取隊列頭節點元素並與新加入元素進行比較
                leader = null;          //Q2爲何要將leader線程置爲null且喚起等待隊列
                available.signal();     
                }
                return true;
            } finally {
                lock.unlock();
            }
        }

offer方法比較簡單,只針對以上兩處做詳細說明
Q1和Q2兩個操作都是爲了解決一個問題,就是leader對應隊列首節點元素的問題,因爲元素是不斷在加入的,比如,leader對應需要取出的首節點是A,此時A雖然是首節點元素,但是還沒有到達延遲時間,所以leader還在等待A,他們的關係是對應的(對應關係的邏輯參考take()源碼),那麼此時加入了元素B,這時候元素B排在了隊首,那麼此時需要處理元素B的就不再是當前的leader了,所以我們需要將leader置空,重新選取新的leader來處理這個B,至於之前的leader線程,在take源碼中,在調用available.awaitNanos(delay)後,當時間到了會重新獲取鎖然後執行操作

所以我們要首先判斷加入的新元素是否是首節點,以便確定對應線程的處理關係

絕大多數的文章對源碼中爲什麼進行if (q.peek() == e)和leader = null的操作的原因隻字不提,我覺得還是有必要寫下的,我對於此處原因的理解可能也存在偏差,希望各位不吝賜教

take方法:取出元素並處理元素事件

/**
     * 首先獲取優先隊列的首個元素,如果爲空則調用線程沉睡。
     * 如果優先級隊列不爲空,查看當前首元素是否到達過期時間,到達過期時間了就獲取並移除隊列
     * 如果沒有到達過期時間,將first變量置爲null(防止內存泄漏),如果leader線程不爲空則進入等待隊列
     * 如果leader爲空,則當前線程爲leader,並限時進入等待隊列中進行等待
     * 如果leader爲空,隊列中還有元素存在,則喚醒所有等待的follower線程
     * 繼續循環,直到獲取延時隊列中的元素
     */
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                E first = q.peek(); //獲取優先隊列中的首個節點
                if (first == null)  //如果優先隊列中沒有節點,則該線程進入等待線程
                    available.await();
                else {  //如果首節點不爲空
                    long delay = first.getDelay(NANOSECONDS);   //獲取當前元素還需要延時多長時間
                    if (delay <= 0) //如果延時時間小於或是等於0,則移出隊列
                        return q.poll();
                    first = null; // don't retain ref while waiting防止內存泄漏
                    if (leader != null) //說明leader線程正在工作,當前線程就進入等待隊列中
                        available.await();//當前線程轉變爲follower線程
                    else {  //如果首節點不爲空,延時時間還沒到,沒有相應的處理線程
                        Thread thisThread = Thread.currentThread(); //獲取當前線程
                        leader = thisThread;    //當前線程設置爲首線程
                        try {
                            available.awaitNanos(delay);    //限時進入等待隊列中處理延時時間最小的元素,並釋放鎖
                        } finally {
                            if (leader == thisThread)
                                leader = null;  //執行事件之後,將leader線程置爲null讓給其他線程
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null) //如果leader線程爲null,優先級隊列中還有元素,則喚醒通知隊列中的線程
                available.signal();
            lock.unlock();
        }
    }

take方法是DelayQueue的核心方法,獲取延遲隊列中的元素,檢索並移除這個隊列的頭部,等待直到這個隊列的過期元素可用
關於源碼的疑惑,不將first=null爲什麼會導致內存泄露?
核心點在於leader調用await方法時會釋放鎖,比如,當線程A獲取了first,然後將當前線程設爲leader線程,接着進入await方法,釋放鎖,這時線程B也獲取了first,因爲leader != null,所以進入阻塞隊列,這時線程A從等待隊列中返回,獲取對象釋放first,但由於線程B中依然有first的引用,所以gc無法對first進行回收,導致內存的泄露

在DelayQueue還有很多值得研究的源碼和問題,我在日後也會慢慢的加上來,第一次寫先寫這麼多吧,不足之處希望可以共同討論進步!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章