面試侃集合 | DelayQueue篇

面試官:好久不見啊局扶,上次我們聊完了PriorityBlockingQueue吉执,今天我們?cè)賮砹牧暮退嚓P(guān)的DelayQueue吧挺益。

Hydra:就知道你前面肯定給我挖了坑,DelayQueue也是一個(gè)無界阻塞隊(duì)列宪卿,但是和之前我們聊的其他隊(duì)列不同的诵,不是所有類型的元素都能夠放進(jìn)去,只有實(shí)現(xiàn)了Delayed接口的對(duì)象才能放進(jìn)隊(duì)列佑钾。Delayed對(duì)象具有一個(gè)過期時(shí)間西疤,只有在到達(dá)這個(gè)到期時(shí)間后才能從隊(duì)列中取出。

面試官:有點(diǎn)意思休溶,那么它有什么使用場(chǎng)景呢代赁?

Hydra:不得不說,由于DelayQueue的精妙設(shè)計(jì)兽掰,使用場(chǎng)景還是蠻多的芭碍。例如在電商系統(tǒng)中,如果有一筆訂單在下單30分鐘內(nèi)沒有完成支付孽尽,那么就需要自動(dòng)取消這筆訂單窖壕。還有,如果我們緩存了一些數(shù)據(jù)泻云,并希望這些緩存在一定時(shí)間后失效的話艇拍,也可以使用延遲隊(duì)列將它從緩存中刪除。

以電商系統(tǒng)為例宠纯,可以簡(jiǎn)單看一下這個(gè)流程:

image

面試官:看起來和任務(wù)調(diào)度有點(diǎn)類似啊,它們之間有什么區(qū)別嗎层释?

Hydra:任務(wù)調(diào)度更多的偏向于定時(shí)的特性婆瓜,是在指定的時(shí)間點(diǎn)時(shí)間間隔執(zhí)行特定的任務(wù),而延遲隊(duì)列更多偏向于在指定的延遲時(shí)間后執(zhí)行任務(wù)。相對(duì)任務(wù)調(diào)度來說廉白,上面舉的例子中的延遲隊(duì)列場(chǎng)景都具有高頻率的特性个初,使用定時(shí)任務(wù)來實(shí)現(xiàn)它們的話會(huì)顯得有些過于笨重了。

面試官:好了猴蹂,你也白話了半天了院溺,能動(dòng)手就別吵吵,還是先給我寫個(gè)例子吧磅轻。

Hydra:好嘞珍逸,前面說過存入隊(duì)列的元素要實(shí)現(xiàn)Delayed接口,所以我們先定義這么一個(gè)類:

public class Task implements Delayed {
    private String name;
    private long delay,expire;
    public Task(String name, long delay) {
        this.name = name;
        this.delay = delay;
        this.expire=System.currentTimeMillis()+delay;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
    @Override
    public int compareTo(Delayed o) {
        return (int)(this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }

}

實(shí)現(xiàn)了Delayed接口的類必須要實(shí)現(xiàn)下面的兩個(gè)方法:

  • getDelay方法用于計(jì)算對(duì)象的剩余延遲時(shí)間聋溜,判斷對(duì)象是否到期谆膳,計(jì)算方法一般使用過期時(shí)間減當(dāng)前時(shí)間。如果是0或負(fù)數(shù)撮躁,表示延遲時(shí)間已經(jīng)用完漱病,否則說明還沒有到期

  • compareTo方法用于延遲隊(duì)列的內(nèi)部排序比較,這里使用當(dāng)前對(duì)象的延遲時(shí)間減去被比較對(duì)象的延遲時(shí)間

在完成隊(duì)列中元素的定義后把曼,向隊(duì)列中加入5個(gè)不同延遲時(shí)間的對(duì)象杨帽,并等待從隊(duì)列中取出:

public void delay() throws InterruptedException {
    DelayQueue<Task> queue=new DelayQueue<>();
    queue.offer(new Task("task1",5000));
    queue.offer(new Task("task2",1000));
    queue.offer(new Task("task3",6000));
    queue.offer(new Task("task4",100));
    queue.offer(new Task("task5",3000));

    while(true){
        Task task = queue.take();
        System.out.println(task);
    }
}

運(yùn)行結(jié)果如下,可以看到按照延遲時(shí)間從短到長(zhǎng)的順序嗤军,元素被依次從隊(duì)列中取出睦尽。

Task{name='task4', delay=100}
Task{name='task2', delay=1000}
Task{name='task5', delay=3000}
Task{name='task1', delay=5000}
Task{name='task3', delay=6000}

面試官:看起來應(yīng)用還是挺簡(jiǎn)單的,今天也不能這么草草了事吧型雳,還是說說原理吧当凡。

Hydra:開始的時(shí)候你自己不都說了嗎,今天咱們聊的DelayQueue和前幾天聊過的PriorityBlockingQueue多少有點(diǎn)關(guān)系纠俭。DelayQueue的底層是PriorityQueue沿量,而PriorityBlockingQueue和它的差別也沒有多少,只是在PriorityQueue的基礎(chǔ)上加上鎖和條件等待冤荆,入隊(duì)和出隊(duì)用的都是二叉堆的那一套邏輯朴则。底層使用的有這些:

private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
private Thread leader = null;
private final Condition available = lock.newCondition();

面試官:你這樣也有點(diǎn)太糊弄我了吧,這就把我敷衍過去了钓简?

Hydra:還沒完呢乌妒,還是先看入隊(duì)的offer方法,它的源碼如下:

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        q.offer(e);
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}

DelayQueue每次向優(yōu)先級(jí)隊(duì)列PriorityQueue中添加元素時(shí)外邓,會(huì)以元素的剩余延遲時(shí)間delay作為排序的因素撤蚊,來實(shí)現(xiàn)使最先過期的元素排在隊(duì)首,以此達(dá)到在之后從隊(duì)列中取出的元素都是先取出最先到達(dá)過期的元素损话。

二叉堆的構(gòu)造過程我們上次講過了侦啸,就不再重復(fù)了槽唾。向隊(duì)列中添加完5個(gè)元素后,二叉堆和隊(duì)列中的結(jié)構(gòu)是這樣的:

image

當(dāng)每個(gè)元素在按照二叉堆的順序插入隊(duì)列后光涂,會(huì)查看堆頂元素是否剛插入的元素庞萍,如果是的話那么設(shè)置leader線程為空,并喚醒在available上阻塞的線程忘闻。

這里先簡(jiǎn)單的介紹一下leader線程的作用钝计,leader是等待獲取元素的線程,它的作用主要是用于減少不必要的等待齐佳,具體的使用在后面介紹take方法的時(shí)候我們細(xì)說私恬。

面試官:也別一會(huì)了,趁熱打鐵直接講隊(duì)列的出隊(duì)方法吧重虑。

Hydra:這還真沒法著急践付,在看阻塞方法take前還得先看看非阻塞的poll方法是如何實(shí)現(xiàn)的:

public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        E first = q.peek();
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            return q.poll();
    } finally {
        lock.unlock();
    }
}

代碼非常短,理解起來非常簡(jiǎn)單缺厉,在加鎖后首先檢查堆頂元素永高,如果堆頂元素為空或沒有到期,那么直接返回空提针,否則返回堆頂元素命爬,然后解鎖。

面試官:好了辐脖,鋪墊完了吧饲宛,該講阻塞方法的過程了吧?

Hydra:阻塞的take方法理解起來會(huì)比上面稍微困難一點(diǎn)嗜价,我們還是直接看它的源碼:

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)
                    return q.poll();
                first = null; // don't retain ref while waiting
                if (leader != null)
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}

阻塞過程中分支條件比較復(fù)雜艇抠,我們一個(gè)一個(gè)看:

  • 首先獲取堆頂元素,如果為空久锥,那么說明隊(duì)列中還沒有元素家淤,讓當(dāng)前線程在available上進(jìn)行阻塞等待
  • 如果堆頂元素不為空,那么查看它的過期時(shí)間瑟由,如果已到期絮重,那么直接彈出堆頂元素
  • 如果堆頂元素還沒有到期,那么查看leader線程是否為空歹苦,如果leader線程不為空的話青伤,表示已經(jīng)有其他線程在等待獲取隊(duì)列的元素,直接阻塞當(dāng)前線程殴瘦。
  • 如果leader為空狠角,那么把當(dāng)前線程賦值給它,并調(diào)用awaitNanos方法痴施,在阻塞delay時(shí)間后自動(dòng)醒來擎厢。喚醒后究流,如果leader還是當(dāng)前線程那么把它置為空辣吃,重新進(jìn)入循環(huán)动遭,再次判斷堆頂元素是否到期。

當(dāng)有隊(duì)列中的元素完成出隊(duì)后神得,如果leader線程為空厘惦,并且堆中還有元素,就喚醒阻塞在available上的其他線程哩簿,并釋放持有的鎖宵蕉。

面試官:我注意到一個(gè)問題,在上面的代碼中节榜,為什么要設(shè)置first = null呢羡玛?

Hydra:假設(shè)有多個(gè)線程在執(zhí)行take方法,當(dāng)?shù)谝粋€(gè)線程進(jìn)入時(shí)宗苍,堆頂元素還沒有到期稼稿,那么會(huì)將leader指向自己,然后阻塞自己一段時(shí)間讳窟。如果在這期間有其他線程到達(dá)让歼,會(huì)因?yàn)?code>leader不為空阻塞自己。

當(dāng)?shù)谝粋€(gè)線程阻塞結(jié)束后丽啡,如果將堆頂元素彈出成功谋右,那么first指向的元素應(yīng)該被gc回收掉。但是如果還被其他線程持有的話补箍,它就不會(huì)被回收掉改执,所以將first置為空可以幫助完成垃圾回收。

面試官:我突然有一個(gè)發(fā)散性的疑問坑雅,定時(shí)任務(wù)線程池ScheduledThreadPoolExecutor辈挂,底層使用的也是DelayQueue嗎?

Hydra:?jiǎn)栴}很不錯(cuò)霞丧,但很遺憾并不是呢岗,ScheduledThreadPoolExecutor在類中自己定義了一個(gè)DelayedWorkQueue內(nèi)部類,并沒有直接使用DelayQueue蛹尝。不過如果你看一下源碼后豫,就會(huì)看到它們實(shí)現(xiàn)的邏輯基本一致,同樣是基于二叉堆的上浮突那、下沉挫酿、擴(kuò)容,也同樣基于leader愕难、鎖早龟、條件等待等操作惫霸,只不過自己用數(shù)組又實(shí)現(xiàn)了一遍而已。說白了葱弟,看看兩個(gè)類的作者壹店,都是Doug Lea大神,所以差異根本沒有多大芝加。

面試官:好了硅卢,今天先到這吧,能最后再總結(jié)一下嗎藏杖?

Hydra:DelayQueue整體理解起來也沒有什么困難的點(diǎn)将塑,難的地方在前面聊優(yōu)先級(jí)隊(duì)列的時(shí)候基本已經(jīng)掃清了,新加的東西也就是一個(gè)對(duì)于leader線程的操作蝌麸,使用了leader線程來減少不必要的線程等待時(shí)間点寥。

面試官:今天的面試有點(diǎn)短啊,總是有點(diǎn)意猶未盡的感覺来吩,看來下次得給你加點(diǎn)料了敢辩。

Hydra:……

如果文章對(duì)您有所幫助,歡迎關(guān)注公眾號(hào) 碼農(nóng)參上

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末误褪,一起剝皮案震驚了整個(gè)濱河市责鳍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兽间,老刑警劉巖历葛,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異嘀略,居然都是意外死亡恤溶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門帜羊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咒程,“玉大人,你說我怎么就攤上這事讼育≌室觯” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵奶段,是天一觀的道長(zhǎng)饥瓷。 經(jīng)常有香客問我,道長(zhǎng)痹籍,這世上最難降的妖魔是什么呢铆? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮蹲缠,結(jié)果婚禮上棺克,老公的妹妹穿的比我還像新娘悠垛。我一直安慰自己,他們只是感情好娜谊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布确买。 她就那樣靜靜地躺著,像睡著了一般因俐。 火紅的嫁衣襯著肌膚如雪拇惋。 梳的紋絲不亂的頭發(fā)上周偎,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天抹剩,我揣著相機(jī)與錄音,去河邊找鬼蓉坎。 笑死澳眷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蛉艾。 我是一名探鬼主播钳踊,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼勿侯!你這毒婦竟也來了拓瞪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤助琐,失蹤者是張志新(化名)和其女友劉穎祭埂,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兵钮,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛆橡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了掘譬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泰演。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖葱轩,靈堂內(nèi)的尸體忽然破棺而出睦焕,到底是詐尸還是另有隱情,我是刑警寧澤靴拱,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布垃喊,位于F島的核電站,受9級(jí)特大地震影響缭嫡,放射性物質(zhì)發(fā)生泄漏缔御。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一妇蛀、第九天 我趴在偏房一處隱蔽的房頂上張望耕突。 院中可真熱鬧笤成,春花似錦、人聲如沸眷茁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)上祈。三九已至培遵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間登刺,已是汗流浹背籽腕。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留纸俭,地道東北人皇耗。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像揍很,于是被迫代替她去往敵國(guó)和親郎楼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容