DelayQueue之源码分析

本文将会对DelayQueue做一个简单的介绍,并提供部分源码的分析。

DelayQueue的特性基本上由BlockingQueue、PriorityQueue和Delayed的特性来决定的。

简而言之,DelayQueue是通过Delayed,使得不同元素之间能按照剩余的延迟时间进行排序,然后通过PriorityQueue,使得超时的元素能最先被处理,然后利用BlockingQueue,将元素处理的操作阻塞住。

基本定义如下:

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {
    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();
}

ReentrantLock lock = new ReentrantLock();
ReentrantLock是一个可重入的互斥锁,将由最近成功获得锁,并且还没有释放该锁的线程所拥有,当锁被其他线程获得时,调用lock的线程将无法获得锁。
在DelayQueue中,只有一个互斥锁lock。

PriorityQueue<E> q = new PriorityQueue<E>();
PriorityQueue是一个优先级队列,每次从队列中取出的是具有最高优先权的元素。
在DelayQueue中,因为E继承于Delayed,所以q表示一个按照delayTime排序的优先级队列,用于存放需要延迟执行的元素。

Thread leader = null;
这里的leader设计出来是为了minimize unnecessary timed waiting(减少不必要的等待时间),如何实现的方案会在详细解读中解释。
在DelayQueue中leader表示一个等待从队列中获取消息的线程。

Condition available = lock.newCondition();
Condition是lock对象的条件变量,只能和锁lock配合使用,用于控制并发程序访问竞争资源的安全。
一个锁lock可以有多个条件变量condition,每个条件上可以有多个线程等待,通过调用await()方法,可以让线程在该条件下等待。当调用signalAll()方法,又可以唤醒该条件下的等待的线程。
在DelayQueue中lock对象只有一个条件变量available。

以下是DelayQueue的主要方法:

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();
    }
}

1、执行lock.lock(),获取锁。

2、把元素e添加到优先队列q(下称队列q)中。

3、判断队列q的队首元素是否为e。

4、如果e是队首元素的话,即元素e是最近可被执行的元素,意味着延迟队列的执行顺序将被变更。
执行leader = null,否则在执行take时,所有线程就会在if(leader!=null)的判断下进入等待。
执行available.signal(),唤醒其他等待中的线程,重新去循环执行take中的操作1-8。
如果不执行signal,那么在take方法中,只有执行awaitNanos(delay)的线程在等待delay指定的时间后自动唤醒,其他执行await的线程将一直被挂起。
如果没有新的线程去执行take方法,那么等待执行awaitNanos(delay)的线程自动唤醒时,此时等待时间将超过元素e的delayTime,这不符合预期。
即便有新的线程去执行take方法,那之前挂起的线程也将一直在等待,效率很低。

5、在finally块中执行lock.unlock()。
需要注意的是,锁必须在 finally 块中释放。否则,如果代码抛出异常,那么锁就有可能永远得不到释放。如果没有释放锁,那么就会产生死锁的问题。

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();
    }
}

1、执行lock.lockInterruptibly(),获取锁。
lockInterruptibly和lock的区别在于
lock 在锁被其他线程占有,当前线程等待锁期间(下称等待锁期间),只考虑获取锁。只有在获取锁成功后,才会去响应中断。
而lockInterruptibly 在等待锁期间,会优先考虑响应中断,而不是响应锁的获取。如果当前线程被打断(interrupt)则该方法抛出InterruptedException。该方法提供了一种解除死锁的途径。

2、E first = q.peek(),获取队列q的队首元素first(下称first)。

3、如果first为空,则执行avaliable.await()让线程进入等待。实际上就是释放锁,然后挂起线程,等待被唤醒,此时其他线程可以获得锁了。
await()和awaitNanos(nanosTimeout)区别在于
执行awaitNanos(nanosTimeout)的线程比执行await()的线程多一个唤醒条件,超过等待nanosTimeout指定的时间,线程将自动唤醒。线程唤醒时,保证该线程是持有锁的。

4、如果first不为空,则执行first.getDelay(NANOSECONDS)获取first的剩余延迟时间delayTime(下称delayTime)

5、如果first的delayTime<=0,表明该元素已经达到之前设定的延迟时间了,则调用return q.poll(),将first从队列q中的移除并且返回该元素first.

6、如果first的delayTime>0,则将first指向null,释放first的引用,避免内存泄露.

7、如果线程leader(下称leader)不为空的话,则执行avaliable.await()让线程进入等待。leader不为空的话,表明已经有其他线程在获取优先队列q的队首元素了(下称获取队首元素),此时只需要执行avaliable.await()让当前线程进入等待即可。

8、如果leader为空,则执行Thread thisThread = Thread.currentThread();leader = thisThread;将leader指向当前线程,然后执行available.awaitNanos(delay);让线程最长等待delayTime的时间。最后在finally块中,如果leader依然指向前文获取的当前线程thisThread,那么将leader指向null,释放leader引用。
这里leader为空,表明尚未有其他线程在获取队首元素,此时设置leader对象,指向当前线程(下称currentThread)。因为currentThread执行了available.awaitNanos(delay)释放了锁,所以其他线程(下称otherThread)在调用take方法时能获取锁,但是因为leader非空,所以otherThread都会进入7的那步,直接进入等待,而不需要像currentThread那样执行8的一系列操作,达到设计leader线程的初衷。

9、循环执行以上1-8步,直到first非空且first的delayTime<=0,跳出循环。

10、跳出循环后,进入finally块。

11、如果leader为空且队列q的队首元素非null(q队列中移除了上文的first元素后还有其他元素),此时执行available.signal(),调用signal唤醒其他等待中的线程。

12、执行lock.unlock(),执行解锁操作。

ok,源码分析就先讲到这里了,下一期我准备讲一下如何将DelayQueue封装成可用的组件,让使用者调用起来更加方便。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容