Java并发------并发队列 BlockingQueue

一、BlockingQueue 简介

BlockingQueue 接口继承了 Queue,它是一个先进先出的队列(Queue),为什么说是阻塞(Blocking)的呢?是因为 BlockingQueue 支持当获取队列元素但是队列为空时,会阻塞等待队列中有元素再返回;也支持添加元素时,如果队列已满,那么等到队列可以放入新元素时再放入。

BlockingQueue 对插入操作、移除操作、获取元素操作提供了四种不同的模式,用于应用不同的场景中:1、抛出异常;2、返回特殊值(null 或 true/false,取决于具体的操作);3、阻塞等待此操作,直到这个操作成功;4、阻塞等待此操作,直到成功或者超时指定时间。总结如下:

Throws exceptionSpecial valueBlocksTimes out
Insertadd(e)offer(e)put(e)offer(e, time, unit)
Removeremove()poll()take()poll(time, unit)
Examineelement()peek()not applicablenot applicable

为什么会有这么多不同的模式?因为 Queue 这个接口继承了 Collection ,add、remove 来自 Collection 操作集合的基本操作,它规定了队列容量满了或者容量为 0,会有抛出异常的情况;offer、poll、peek 来自 Queue 操作队列的基本操作,它规定了队列容量满了或者容量为 0,会返回 boolean 值或者 null;put、take 操作来自 BlockingQueue 操作队列的基本操作,它规定了队列容量满了或者容量为 0,会把当前线程阻塞。所以学习 BlockingQueue 我们重点还是要关注 put、take 这两个操作。

BlockingQueue 有一下几个特点:

  1. BlockingQueue 不接受 null 值的插入,插入 null 时会抛出 NullPointerException 异常。null 值在这里通常用于作为特殊值返回(表格中的第三列),代表 poll 失败。
  2. BlockingQueue 并不是绝对无界的,容量最多是 Integer.MAX_VALUE,所以通常说是无界的。
  3. BlockingQueue 的实现都是线程安全的,但是批量的集合操作如 addAll, containsAll, retainAll 和 removeAll 不一定是原子操作。如 addAll© 有可能在添加了一些元素后中途抛出异常,此时 BlockingQueue 中已经添加了部分元素,这个是允许的,取决于具体的实现。

二、ArrayBlockingQueue

ArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现。它的构造函数中,初始化的容量大小是必须指定的,其他可选:是否使用公平锁、指定集合初始化后添加进数组中。

ArrayBlockingQueue 的几个基本属性:

//存放元素的数组
final Object[] items;
//下次读取操作的位置
int takeIndex;
//下次写入操作的位置
int putIndex;
//已有元素的个数
int count;
//并发控制的锁
final ReentrantLock lock;
private final Condition notEmpty;
private final Condition notFull;

看到 ReentrantLock 我们就应该明白 ArrayBlockingQueue 的同步原理,前面讲 Condition 的使用时就是举的这个例子,读操作和写操作都需要获取到 AQS 独占锁才能进行操作。队列为空,线程进入到读线程队列排队;队列已满,线程进入到写线程队列排队。在新加入元素或者取出元素后才唤醒阻塞的队列。

三、LinkedBlockingQueue

注意:jdk7 和 jdk8 的实现方式上有些许不同。下面只讲 jdk8 的 LinkedBlockingQueue

LinkedBlockingQueue 底层采用链表的方式实现的有界或者无界队列,构造函数中可以指定初始值也可以不指定,内部使用 Node 来实现链表的方式。jdk7 是单向链表,jdk8 是双向链表实现,其实就是有前指针和后指针了。

LinkedBlockingQueue 的几个基本属性:

//队头
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();

jdk7 是使用两个 ReentrantLock 和两个 Condition来实现的,比 jdk8 的实现会复杂一点。

简单的看一下 put 方法:

public void putFirst(E e) throws InterruptedException {
    //不允许添加null值
    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();
    }
}
private boolean linkFirst(Node<E> node) {
    //元素个数超过设置容量
    if (count >= capacity)
        return false;
    Node<E> f = first;
    node.next = f;
    first = node;
    if (last == null)
        last = node;
    else
        f.prev = node;
    ++count;
    notEmpty.signal();
    return true;
}

仔细看一下,offer、put、add 其实都是调用 link 方法的,poll、take、remove 调用的是 unlink 方法。jdk7 是没有 First 和 Last 操作的,属于 jdk8 拓展了链表的使用。而进入到 link 方法前已经获取独占锁了,可以保证线程安全,所以内部操作直接进行修改值操作就可以了。

了解了 ReentrantLock 原理,上面的代码还是很简单的,主要在 link() 方法中检查当前元素个数 count 是否达到设置容量 capacity 或者是否是 0,返回 false,调用 await 阻塞线程,等待唤醒;返回 true,直接操作。

四、SynchronousQueue

SynchronousQueue 是一种特殊的队列,它不像前两种队列中内部有存储元素的数组或者链表,SynchronousQueue 内部不提供存储元素的元器。它的名字就蕴含了它的特征:同步队列,这里同步的意义不是说多线程的并发,而是说当进行写操作时,不会立即返回,需要等待另一条读操作线程进来时,才会返回,反之亦然。

SynchronousQueue 不存储元素,但是它的内部会使用类似于等待队列的 Transferer 来管理读写操作的线程。当多条操作类型相同的线程进来时,Transferer 会使用单向链表 SNode 来存储线程,将线程一一挂起,等待操作类型匹配的线程进来唤醒。

下面看一下 SynchronousQueue 的构造函数和读写操作:

//SynchronousQueue构造函数
public SynchronousQueue() {
    this(false);
}
//TransferQueue转移队列,意味公平的原则;TransferStack转移堆栈,意味着非公平的原则
public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}
//写操作
public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    if (transferer.transfer(e, false, 0) == null) {
        Thread.interrupted();
        throw new InterruptedException();
    }
}
//读操作
public E take() throws InterruptedException {
    E e = transferer.transfer(null, false, 0);
    if (e != null)
        return e;
    Thread.interrupted();
    throw new InterruptedException();
}

SynchronousQueue 完成相应读写操作是靠 Transferer 的 transfer 方法实现的,不同类型的操作主要看第一个参数是否是 null。 Transferer 共有两个实现类,其实就是公平与非公平的区别,下面看 TransferQueue 的 transfer 方法:

E transfer(E e, boolean timed, long nanos) {
    //文档翻译:根据需要重建或者重构
    QNode s = null;
    //e为null是读线程,e不为null是写线程
    boolean isData = (e != null);
    for (;;) {
        QNode t = tail;
        QNode h = head;
        if (t == null || h == null)         // saw uninitialized value
            continue;                       // spin
        //队列为空或者新加入元素的读写类型与队列中的一致,也就说需要挂起线程
        if (h == t || t.isData == isData) { // empty or same-mode
            QNode tn = t.next;
            //有节点刚刚入队,再循环一次,只拿等待队列的尾节点
            if (t != tail)                  // inconsistent read
                continue;
            //有节点入队,但是还没来及修改tail,重新设置tail即可
            if (tn != null) {               // lagging tail
                advanceTail(t, tn);
                continue;
            }
            if (timed && nanos <= 0)        // can't wait
                return null;
            //将新传入的e包装成QNode
            if (s == null)
                s = new QNode(e, isData);
            //将新生成的SNode,插入到tail的下一个节点中,失败的话代表有其他几点刚刚插入
            if (!t.casNext(null, s))        // failed to link in
                continue;
            //更新tail
            advanceTail(t, s);              // swing tail and wait
            //挂起后又被唤醒
            Object x = awaitFulfill(s, e, timed, nanos);
            if (x == s) {                   // wait was cancelled
                clean(t, s);
                return null;
            }
            if (!s.isOffList()) {           // not already unlinked
                advanceHead(t, s);          // unlink if head
                if (x != null)              // and forget fields
                    s.item = s;
                s.waiter = null;
            }
            return (x != null) ? (E)x : e;
        //说明新加入元素的读写类型正好匹配,直接写入元素或者读出元素即可
        } else {                            // complementary-mode
            QNode m = h.next;               // node to fulfill
            if (t != tail || m == null || h != head)
                continue;                   // inconsistent read
            Object x = m.item;
            //正常的话会修改x的item值,用于唤醒后退出循环使用
            if (isData == (x != null) ||    // m already fulfilled
                    x == m ||                   // m cancelled
                    !m.casItem(x, e)) {         // lost CAS
                advanceHead(h, m);          // dequeue and retry
                continue;
            }
            //将head设置为等待队列第一个节点
            advanceHead(h, m);              // successfully fulfilled
            //唤醒这个节点的线程
            LockSupport.unpark(m.waiter);
            return (x != null) ? (E)x : e;
        }
    }
}
Object awaitFulfill(QNode s, E e, boolean timed, long nanos) {
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    Thread w = Thread.currentThread();
    int spins = ((head.next == s) ?
            (timed ? maxTimedSpins : maxUntimedSpins) : 0);
    for (;;) {
        if (w.isInterrupted())
            s.tryCancel(e);
        Object x = s.item;
        //这是唯一的出口,判断条件就是item是否还是e
        if (x != e)
            return x;
        if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                s.tryCancel(e);
                continue;
            }
        }
        if (spins > 0)
            --spins;
        else if (s.waiter == null)
            s.waiter = w;
        //将线程挂起等待唤醒
        else if (!timed)
            LockSupport.park(this);
        else if (nanos > spinForTimeoutThreshold)
            LockSupport.parkNanos(this, nanos);
    }
}

不一定要把所有方法都看懂,看出来怎么挂起,怎么退出循环即可,所有的读写操作都在这个方法中完成,并且有唤醒等待线程的操作,很简洁,但是不好读。TransferStack 代码会稍复杂一点,原理也差不多。

SynchronousQueue 记住最重要的一点就可以了,它本身不会存储任何元素,所以不能使用 peek 方法,也不可以拿来当普通的 Collection 来用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值