三十八、并发编程之阻塞队列LinkedBlockingQueue原理简析

一、LinkedBlockingQueue简介

基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue对象,而没有指定其容量大小,LinkedBlockingQueue会默认一个类似无限大小的容量(Integer.MAX_VALUE),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

二、初识LinkedBlockingQueue

public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable{
    static class Node<E> {
        E item;
        Node<E> next;//下一个节点
        Node(E x) { item = x; }
    }

    private final int capacity;//容量大小
    private final AtomicInteger count = new AtomicInteger();//用来对队列的Node数量同步计数
    transient Node<E> head;//头节点
    private transient Node<E> last;//尾节点

    private final ReentrantLock takeLock = new ReentrantLock();//取锁
    private final Condition notEmpty = takeLock.newCondition();
    private final ReentrantLock putLock = new ReentrantLock();//放锁
    private final Condition notFull = putLock.newCondition();
}

public interface BlockingQueue<E> extends Queue<E> {
    boolean add(E e);
    boolean offer(E e);
    void put(E e) throws InterruptedException;
    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
    E take() throws InterruptedException;
    E poll(long timeout, TimeUnit unit) throws InterruptedException;
    int remainingCapacity();
    boolean remove(Object o);
    public boolean contains(Object o);
    int drainTo(Collection<? super E> c);
    int drainTo(Collection<? super E> c, int maxElements);
}

可以看到LinkedBlockingQueue内部有两个可重入独占锁takeLock 和putLock ,从字面就可以判定这两个锁是控制插入和取出操作同步的。两个Condition变量分别是notEmpty和notFull,猜测一个是当队列为空时让取出操作阻塞,一个是当队列已满时让插入操作阻塞,就像一个支持多线程插入与取出的生产者消费者模型。

LinkedBlockingQueue内部还有一个Node类以及Node类型的head和last变量,可见它是以单向链表实现的。AtomicInteger类型的cout用来对队列的Node数量同步计数。

三、方法原理

1、阻塞插入操作 put

 	public void put(E e) throws InterruptedException {
 		//e为空抛出空指针异常
        if (e == null) throw new NullPointerException();
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();//插入加锁
        try {
            while (count.get() == capacity) {//队列满了
                notFull.await();//插入队列的线程等待
            }
            enqueue(node);//插入队列尾部
            c = count.getAndIncrement();
            //如果插入队列尾部后队列的长度依然小于上限就执行notFull的signal方法唤醒某个等待插入队列的线程
            if (c + 1 < capacity) 
                notFull.signal();//唤醒某个等待插入队列的线程
        } finally {
            putLock.unlock();//释放锁
        }
        //c == 0可能存在取出操作的线程阻塞在notEmpty的条件上,通过signalNotEmpty去唤醒一个挂起的线程
        if (c == 0)
            signalNotEmpty();
    }

这里用了putLock来加锁,也就是插入操作是同步执行的,当队列已满通过notFull条件来挂起线程。如果未满就执行enqueue (node)插入队列尾部,然后将队列长度同步+1:

    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        last = last.next = node;
    }

如果插入队列尾部后队列的长度依然小于上限就执行notFull的signal方法唤醒某个等待插入队列的线程。该线程是队列满了以后阻塞在notFull条件上的。

如果c==0发生,表示可能存在取出操作的线程阻塞在notEmpty的条件上,通过signalNotEmpty去唤醒一个挂起的线程。

 private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();//加锁
        try {
            notEmpty.signal();//唤醒
        } finally {
            takeLock.unlock();//释放锁
        }
    }

2、阻塞取出 take

public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();//取出加锁
        try {
            while (count.get() == 0) {//队列为空
                notEmpty.await();取出队列的线程等待
            }
            x = dequeue();//取出队列头部
            c = count.getAndDecrement();
            //如果队列不为空,通过notEmpty条件的signal方法唤醒某个等待取出节点的线程让它开始工作。该线程是队列为空时阻塞在notEmpty上的。
            if (c > 1)
                notEmpty.signal();//唤醒
        } finally {
            takeLock.unlock();//释放锁
        }
        //判断队列的长度刚好满足上限,表示可能有挂起的等待插入的线程,通过signalNotFull方法将其中一个唤醒
        if (c == capacity)
            signalNotFull();
        return x;
    }

先通过takeLock加锁,表示该操作是同步执行的,如果当前队列长度为0则通过notEmpty条件挂起线程。
如果当前队列长度不为0,取出头部的节点,然后将队列长度同步-1:

 private E dequeue() {
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

如果队列不为空,通过notEmpty条件的signal方法唤醒某个等待取出节点的线程让它开始工作。该线程是队列为空时阻塞在notEmpty上的。

最后判断队列的长度刚好满足上限,表示可能有挂起的等待插入的线程,通过signalNotFull方法将其中一个唤醒。

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();//加锁
        try {
            notFull.signal();//唤醒
        } finally {
            putLock.unlock();//释放锁
        }
    }

阻塞插入取出小结:
可以发现插入与插入操作之间是同步的,取出与取出操作之间也是同步的,而插入与取出是并发的,也就是在一个线程在执行插入到尾部时,另一个线程可能正在取头部。这样能提供LinkedBlockingQueue的吞吐量。

使用signal方法而不使用signalAll方法可以提高性能,LinkedBlockingQueue同时最多只能有一个线程执行插入操作,一个线程执行取操作,并且插入的节点数只能一个,所以即使竞争不充分也不会死锁。

3、非阻塞插入 offer

	public boolean offer(E e) {
		//如果e为空,则抛出空指针异常
        if (e == null) 
        	throw new NullPointerException();
        final AtomicInteger count = this.count;
        //队列已满返回false
        if (count.get() == capacity)
            return false;
        int c = -1;
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        putLock.lock();//加锁
        try {
        	//队列未满,队尾插入
            if (count.get() < capacity) {
                enqueue(node);//队尾插入
                c = count.getAndIncrement();
                //c+1还是小于队列容量
                if (c + 1 < capacity)
                    notFull.signal();//唤醒插入线程
            }
        } finally {
            putLock.unlock();//释放锁
        }
        if (c == 0)
            signalNotEmpty();//唤醒等待notEmpty条件的线程
        return c >= 0;
    }

从代码看该方法首先判断了当前队列是否已满,若满了就直接返回false,而不是在同步快中挂起等待notFull条件,同样他也会在执行完同步插入后尝试将等待notFull条件的线程唤醒,并且尝试唤醒等待notEmpty条件的线程。

4、非阻塞取出 poll

	public E poll() {
        final AtomicInteger count = this.count;
        //队列为空返回null
        if (count.get() == 0)
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();//加锁
        try {
            if (count.get() > 0) {
                x = dequeue();//取出队列头部
                c = count.getAndDecrement();
                if (c > 1)
                    notEmpty.signal();//唤醒取出线程
            }
        } finally {
            takeLock.unlock();//是否锁
        }
        if (c == capacity)
            signalNotFull();//唤醒等待notFull条件的线程
        return x;
    }

该方法也是不阻塞的,当队列为空直接返回null。如果能取出节点,在取出节点后也会尝试唤醒等待notEmoty条件的线程,最后也会尝试唤醒等待notFull条件的线程。

5、阻塞不超时插入

	public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
		//e为空,抛出空指针异常
        if (e == null) 
        	throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        putLock.lockInterruptibly();//加锁
        try {
        	队列已满
            while (count.get() == capacity) {
                if (nanos <= 0)
                    return false;
                nanos = notFull.awaitNanos(nanos);// 等待
            }
            enqueue(new Node<E>(e));//队尾插入
            c = count.getAndIncrement();
            if (c + 1 < capacity)
                notFull.signal();//唤醒
        } finally {
            putLock.unlock();// 释放锁
        }
        if (c == 0)
            signalNotEmpty();
        return true;
    }

同阻塞插入相比其实就是在notFull执行await方法时加入了超时时间,通过awaitNanos实现,这样超过timeout队列还是满的就返回false。

6、阻塞不超时取出

 public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        int c = -1;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();//加锁
        try {
        	//队列为空
            while (count.get() == 0) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);//等待
            }
            x = dequeue();// 取出队列头部
            c = count.getAndDecrement();
            if (c > 1)
                notEmpty.signal();//唤醒
        } finally {
            takeLock.unlock();//释放锁
        }
        if (c == capacity)
            signalNotFull();
        return x;
    }

同阻塞取出相比,在notEmpty条件执行awaitNanos方法代替await方法,当超过timeout队列还是空的就返回false。

原文

你可能感兴趣的:(并发编程,并发编程)