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还有很多值得研究的源码和问题,我在日后也会慢慢的加上来,第一次写先写这么多吧,不足之处希望可以共同讨论进步!

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

推荐阅读更多精彩内容