Java并发之阻塞队列

队列

队列是先进先出(FIFO)的线性表。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。

操作 抛出异常 有返回值
Insert add(e) offer(e)
Remove remove() poll()
Examine element() peek()

阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列,这两个附加操作支持阻塞的插入和移除方法.

  • 支持阻塞的插入方法: 当队列满时,队列会阻塞插入元素的线程,直到队列不满为止.
  • 支持阻塞的移除方法: 当队列为空时,获取元素的线程阻塞等待线程非空.

阻塞队列通常用于生产者和消费者的场景,生产者就是向队列里添加元素,而消费者就是从队列里取出元素. 阻塞队列就是生产者存储元素而消费者用来获取元素的容器.

操作方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入 add(e) offer(e) put(e) offer(e,time,unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用

注意: 如果是无界阻塞队列,队列永远都不会出现满的情况,所以使用put或者take方法永远都不会被阻塞,而且使用put方法时,该方法永远返回为true.

JDK提供的阻塞队列

从上面的UML图可以看到,JKD7提供了7个阻塞队列:

  • ArrayBlockingQueue: 由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue: 由链表结构组成的有界阻塞队列
  • PriorityBlockingQueue: 支持优先级排序的无界阻塞队列
  • DelayQueue: 使用优先级队列队列实现的无界阻塞队列
  • SynchronousQueue: 不存储元素的阻塞队列
  • LinkedTransferQueue: 由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque: 由链表结构组成的双向阻塞队列

ArrayBlockingQueue

ArrayBlockingQueue是一个用数组实现的有界队列,此队列按照先进先出的原则对元素进行排序.

默认情况下不保证线程公平的访问队列,所谓公平访问队列是指阻塞的线程,可以按照阻塞的先后顺序访问队列,即先阻塞线程先访问队列.非公平性对先等待的线程是非公平的,当队列可用时,阻塞的线程都可以争夺访问队列的资格,有可能先阻塞的线程最后才访问队列.

为了保证公平性,通常会降低吞吐量,可以使用以下代码创建一个公平的阻塞队列.


    ArrayBlockingQueue fairQueue= new ArrayBlockingQueue(1000,true);

访问者的公平性是使用可重入锁实现的,代码如下:


    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

LinkedBlockingQueue

LinkedBlockingQueue是一个用链表实现的有界阻塞队列,此队列默认最大长度为Integer.MAX_VALUE,按照先进先出(FIFO)的原则对元素进行排序

PriorityBlockingQueue

PriorityBlockingQueue是一个支持优先级的无界阻塞队列,默认情况下元素采用自然排序升序排列,也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序,需要注意的是不能保证同优先级的元素排序.

DelayQueue

DelayQueue是一个支持延时获取元素的无界阻塞队列,队列使用PriorityQueue来实现. 队列中元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素.只有延迟期满时才能从队列中提出元素.

DelayQueue非常有用,可以将DelayQueue运用在一下场景:

  • 缓存系统的设计: 可以送DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦从DelayQueue获取元素,就表示缓存到期了.
  • 定时任务调度:使用DelayQueue保存当前将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimeQueue就是使用DelayQueue实现的.

SynchronousQueue

SynchronousQueue是一个不存储元素的阻塞队列,每个put操作必须等待一个take操作,否则不能继续添加元素.

支持公平访问队列,默认情况下线程采用非公平性策略,使用带boolean参数的构造方法可以实现等待线程采用先进先出(FIFO)的顺序访问队列.


    public SynchronousQueue(boolean fair) {
        transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
    }

LinkedTransferQueue

LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列,相当于其他阻塞队列,LinkedTransferQueue多了一tryTransfer和transfer方法.

  • transfer方法

    如果当前有消费者正在等待接收元素(消费者使用take()方法或者带时间限制的poll方式时)transfer()方法可以吧生产者传入的元素立即transfer(传输)给消费者,如果没有消费者在等待接收元素,transfer方法将元素存放在队列的tail节点,并等待该元素被消费者消费了才返回.

  • tryTransfer方法

    tryTransfer方法用来试探生产者传入元素是否能够直接传递给消费者,如果没有消费者等待接收元素.则返回false, 和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回,而transfer需要等待消费者消费了才返回.

LinkedBlockingDeque

LinkedBlockingDeque是一由链表结构组成的双向阻塞队列,所谓双向队列指的是可以从队列两端插入和移除元素,双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一般竞争.相比其他阻塞队列,LinkedBlockingDeque多了addFirst, addLast,offerFirst,offerLast,peekFirst,peekLast等方法.

在初始化LinkedBlockingDeque时可以设置容量防止其过渡膨胀, 另外,双向阻塞队列可以运行在"工作窃取"模式中.

阻塞队列实现的原理

通知模式实现: 所谓通知模式,就是当生产者从满的队列里添加元素时会阻塞生产者,而当消费者消费了一个队列中的元素后,就会通知生产者当前队列可用. ArrayBlockingQueue使用ReentrantLock和Condition实现.


    /** Main lock guarding all access */
    final ReentrantLock lock;
    /** Condition for waiting takes */
    private final Condition notEmpty;
    /** Condition for waiting puts */
    private final Condition notFull;

     public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

     public void put(E e) throws InterruptedException {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }
    
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;
        if (++putIndex == items.length) putIndex = 0;
        count++;
        notEmpty.signal();
    }

    /**
     * Extracts element at current take position, advances, and signals.
     * Call only when holding lock.
     */
    private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];
        items[takeIndex] = null;
        if (++takeIndex == items.length) takeIndex = 0;
        count--;
        if (itrs != null)
            itrs.elementDequeued();
        notFull.signal();
        return x;
    }

当往队列里插入一个元素时,如果队列不可用,那么阻塞生产者主要通过LockSupport.part(this)实现:


    public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

然后看看LockSupport的源码:发现调研setBlocker先保存一下将要阻塞的线程,然后代用unsafe.park阻塞当前线程:


    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        U.park(false, 0L);
        setBlocker(t, null);
    }

park是个native方法,会阻塞当前线程,只有以下四种情况中一种发生时,该返回才会返回.

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

推荐阅读更多精彩内容

  • 相关文章Java并发编程(一)线程定义、状态和属性 Java并发编程(二)同步Java并发编程(三)volatil...
    刘望舒阅读 5,225评论 1 31
  • 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线...
    端木轩阅读 998评论 0 2
  • 1.阻塞队列定义阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。...
    SDY_0656阅读 426评论 0 1
  • 目前网络上的理财app有很多,投资得收益都是余额宝得几倍有得甚至更高,面对这么多得理财app得时候我们如何去选择,...
    阮策策阅读 248评论 0 0