Java多线程进阶(36)—— J.U.C之collections框架:LinkedBlockingDeque

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

一、LinkedBlockingDeque简介

LinkedBlockingDequeConcurrentLinkedDeque类似,都是一种 双端队列 的结构,只不过LinkedBlockingDeque同时也是一种阻塞队列,它是在JDK1.5时随着J.U.C包引入的,实现了BlockingDueue接口,底层基于 双链表 实现:

注意: LinkedBlockingDeque底层利用ReentrantLock实现同步,并不像ConcurrentLinkedDeque那样采用无锁算法。

另外,LinkedBlockingDeque是一种 近似有界阻塞队列 ,为什么说近似?因为LinkedBlockingDeque既可以在初始构造时就指定队列的容量,也可以不指定,如果不指定,那么它的容量大小默认为Integer.MAX_VALUE

BlockingDeque接口

截止目前为止,我们介绍的阻塞队列都是实现了BlockingQueue接口。和普通双端队列接口——Deque一样,J.U.C中也有一种阻塞的双端队列接口——BlockingDeque。BlockingDeque是JDK1.6时,J.U.C包新增的一个接口:

BlockingDeque的类继承关系图:

我们知道,BlockingQueue中阻塞方法一共有4个:put(e)take()offer(e, time, unit)poll(time, unit),忽略限时等待的阻塞方法,一共就两个:
队尾入队:put(e)
队首出队:take()

BlockingDeque相对于BlockingQueue,最大的特点就是增加了在 队首入队 / 队尾出队 的阻塞方法。下面是两个接口的比较:

阻塞方法BlockingQueueBlockingDeque
队首入队/putFirst(e)
队首出队take()takeFirst()
队尾入队put(e)putLast(e)
队尾出队/takeLast()

二、LinkedBlockingDeque原理

2.1 构造

LinkedBlockingDeque 一共三种构造器,不指定容量时,默认为Integer.MAX_VALUE

    /**
     * 默认构造器.
     */
    public LinkedBlockingDeque() {
        this(Integer.MAX_VALUE);
    }
    /**
     * 指定容量的构造器.
     */
    public LinkedBlockingDeque(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
    }
    /**
     * 从已有集合构造队列.
     */
    public LinkedBlockingDeque(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock lock = this.lock;
        lock.lock(); // Never contended, but necessary for visibility
        try {
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (!linkLast(new Node<E>(e)))
                    throw new IllegalStateException("Deque full");
            }
        } finally {
            lock.unlock();
        }
    }

2.2 内部结构

LinkedBlockingDeque内部是 双链表 的结构,结点Node的定义如下:

    /**
     * 双链表结点定义
     */
    static final class Node<E> {
        /**
         * 结点值, null表示该结点已被移除.
         */
        E item;
    
        /**
         * 前驱结点指针.
         */
        Node<E> prev;
    
        /**
         * 后驱结点指针.
         */
        Node<E> next;
    
        Node(E x) {
            item = x;
        }
    }

字段first指向队首结点,字段last指向队尾结点。另外LinkedBlockingDeque利用 ReentrantLock 来保证线程安全,所有对队列的修改操作都需要先获取这把全局锁:

    public class LinkedBlockingDeque<E> extends AbstractQueue<E>
        implements BlockingDeque<E>, java.io.Serializable {
    
        /**
         * 队首结点指针.
         */
        transient Node<E> first;
    
        /**
         * 队尾结点指针.
         */
        transient Node<E> last;
    
        /**
         * 队列元素个数.
         */
        private transient int count;
    
        /**
         * 队列容量.
         */
        private final int capacity;
    
        /**
         * 全局锁
         */
        final ReentrantLock lock = new ReentrantLock();
    
        /**
         * 出队线程条件队列(队列为空时,出队线程在此等待)
         */
        private final Condition notEmpty = lock.newCondition();
    
        /**
         * 入队线程条件队列(队列为满时,入队线程在此等待)
         */
        private final Condition notFull = lock.newCondition();
    
        //...
    }

2.3 队尾入队——put

先来看下,LinkedBlockingDeque是如何实现正常的从队尾入队的:

    /**
     * 在队尾入队元素e.
     * 如果队列已满, 则阻塞线程.
     */
    public void put(E e) throws InterruptedException {
        putLast(e);
    }
    
    public void putLast(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();    // 队列不能包含null元素
        Node<E> node = new Node<E>(e);                      // 创建入队结点
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            while (!linkLast(node))                         // 队列已满, 则阻塞线程
                notFull.await();
        } finally {
            lock.unlock();
        }
    }

put方法内部调用了putLast方法,这是Deque接口独有的方法。上述入队操作的关键是 linkLast 方法:

    /**
     * 将结点node链接到队尾, 如果失败, 则返回false.
     */
    private boolean linkLast(Node<E> node) {
        // assert lock.isHeldByCurrentThread();
        if (count >= capacity)  // 队列已满, 直接返回
            return false;
    
        // 以下是双链表的"尾插"操作
        Node<E> l = last;
        node.prev = l;
        last = node;
        if (first == null)
            first = node;
        else
            l.next = node;
    
        ++count;            // 队列元素加1
        notEmpty.signal();  // 唤醒一个等待的出队线程
        return true;
    }

linkLast 方法在队尾插入一个结点,插入失败(队列已满的情况)则返回false。插入成功,则唤醒一个正在等待的出队线程:

初始:

队尾插入结点node:


2.4 队首入队——putFirst

队首入队就是双链表的 “头插法” 插入一个结点,如果队列已满,则阻塞调用线程:

    /**
     * 在队首入队元素e.
     * 如果队列已满, 则阻塞线程.
     */
    public void putFirst(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        Node<E> node = new Node<E>(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            while (!linkFirst(node))        // 队列已满, 则阻塞线程
                notFull.await();
        } finally {
            lock.unlock();
        }
    }
    /**
     * 在队首插入一个结点, 插入失败则返回null.
     */
    private boolean linkFirst(Node<E> node) {
        // assert lock.isHeldByCurrentThread();
        if (count >= capacity)      // 队列已满
            return false;
    
        // 以下是双链表的“头插”操作
        Node<E> f = first;
        node.next = f;
        first = node;
        if (last == null)
            last = node;
        else
            f.prev = node;
    
        ++count;                    // 队列元素数量加1
        notEmpty.signal();          // 唤醒一个等待的出队线程
        return true;
    }

初始:

队首插入结点node:


2.5 队首出队——take

队首出队的逻辑很简单,如果队列为空,则阻塞调用线程:

    /**
     * 从队首出队一个元素, 如果队列为空, 则阻塞线程.
     */
    public E take() throws InterruptedException {
        return takeFirst();
    }
    
    public E takeFirst() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            while ((x = unlinkFirst()) == null)     // 队列为空, 则阻塞线程
                notEmpty.await();
            return x;
        } finally {
            lock.unlock();
        }
    }

实际的出队由 unlinkFirst 方法执行:

    /**
     * 从队首删除一个元素, 失败则返回null.
     */
    private E unlinkFirst() {
        // assert lock.isHeldByCurrentThread();
        Node<E> f = first;
        if (f == null)          // 队列为空
            return null;
    
        // 以下是双链表的头部删除过程
        Node<E> n = f.next;
        E item = f.item;
        f.item = null;
        f.next = f;             // help GC
        first = n;
        if (n == null)
            last = null;
        else
            n.prev = null;
    
        --count;                // 队列元素个数减1
        notFull.signal();       // 唤醒一个等待的入队线程
        return item;
    }

初始:

删除队首结点:


2.6 队尾出队——takeLast

队尾出队的逻辑很简单,如果队列为空,则阻塞调用线程:

    /**
     * 从队尾出队一个元素, 如果队列为空, 则阻塞线程.
     */
    public E takeLast() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            while ((x = unlinkLast()) == null)  // 队列为空, 阻塞线程
                notEmpty.await();
            return x;
        } finally {
            lock.unlock();
        }
    }

实际的出队由 unlinkLast 方法执行:

    /**
     * 删除队尾元素, 如果失败, 则返回null.
     */
    private E unlinkLast() {
        // assert lock.isHeldByCurrentThread();
        Node<E> l = last;
        if (l == null)          // 队列为空
            return null;
    
        // 以下为双链表的尾部删除过程
        Node<E> p = l.prev;
        E item = l.item;
        l.item = null;
        l.prev = l;             // help GC
        last = p;
        if (p == null)
            first = null;
        else
            p.next = null;
    
        --count;                // 队列元素个数减1
        notFull.signal();       // 唤醒一个等待的入队线程
        return item;
    }

初始:

删除队尾结点:

三、总结

LinkedBlockingDeque作为一种阻塞双端队列,提供了队尾删除元素和队首插入元素的阻塞方法。该类在构造时一般需要指定容量,如果不指定,则最大容量为Integer.MAX_VALUE。另外,由于内部通过ReentrantLock来保证线程安全,所以LinkedBlockingDeque的整体实现时比较简单的。

另外,双端队列相比普通队列,主要是多了【队尾出队元素】/【队首入队元素】的功能。
阻塞队列我们知道一般用于“生产者-消费者”模式,而双端阻塞队列在“生产者-消费者”就可以利用“双端”的特性,从队尾出队元素。

考虑下面这样一种场景:

有多个消费者,每个消费者有自己的一个消息队列,生产者不断的生产数据扔到队列中,消费者消费数据有快又慢。为了提升效率,速度快的消费者可以从其它消费者队列的 队尾 出队元素放到自己的消息队列中,由于是从其它队列的队尾出队,这样可以减少并发冲突(其它消费者从队首出队元素),又能提升整个系统的吞吐量。这其实是一种“ 工作窃取算法 ”的思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值