阻塞队列(BlockingQueue)源码分析

来源:互联网 发布:c语言用什么编译器好 编辑:程序博客网 时间:2024/05/17 07:21

无意中看到concurrent包中有这个类,随意搜了一下,发现用的人还挺多,故好奇看看其源码和API。
  原来它是一个能在多线程环境下保证并发安全的队列,在解决多生产者-多消费者的场景下特别有用。自身提供4个常用的API,take、put、offer、poll,其实就是2组是读、取,区别在于第一组具备阻塞的能力,第二组通过返回值体现是否成功,类似于Lock的tryLock和lock方法。
  BlockingQueue接口的实现结构有数组和链表等好几种,但是最常用的就是ArrayBlockingQueue和LinkedBlockingQueue。前者的put和take共享一把重入锁,稍微会影响一些效率,但影响不大,且由于长度固定,所以不涉及GC。后者基于链表实现,长度不限,但增删会导致node的增删,在高并发下会涉及到GC的问题,且由于节点长度不限,所以notfull永远不会挂起,可能会造成内存溢出。

 把ArrayBlockingQueue核心的源码贴出来分析下它是如何保证线程安全的(以去掉了一些无关代码):
 首先,通过count和items来表示当前个数和总的容量,同时提供一把ReentrantLock以及2个条件。
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /** The queued items  */
    private final E[] items;
    /** Number of items in the queue */
    private int count;

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

items表示容量,count表示当前个数,lock用于保护线程的并发操作,notEmpty用于实现线程在空时的挂起阻塞,notFull用于实现在满时的挂起阻塞。

然后,看一下put操作的源码是怎么样的:
public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        final E[] items = this.items;
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();                                       //注意,此处通过lock来保护put和take不会并发操作。
        try {
            try {
                while (count == items.length)                           //当满时
                    notFull.await();                                    //非满条件挂起,也就意味着知性put操作的线程会被挂起等待。挂起时,会释放lock锁给其他线程用,比如take

//同时注意,这里是while内的代码,一旦notFull被唤醒,首先要做的事情是判断是否又满了。因为对方发送notify信号到你收到并响应这个过程中,其他线程依然会put数据
//换句话说,信号收到前,又被其他线程放满了,这种情况很常见,因为生产者和消费者都是多个,任意时刻都有可能又某个生产者或者消费者在主动执行。
//但是,一旦信号被接收到后,线程又会重新抢到lock,此时其他线程就无法进行put和take操作了,这时候判断count==items.length是安全可靠的

} catch (InterruptedException ie) {
               ...//略
            }
            insert(e);  //当跳出wihle后,意味着线程执行完了,以put为例,就代表已经成功放入一个元素了,此时可以通知notEmtpy条件,告诉他目前肯定不为空,因为我放了一个
        } finally {
            lock.unlock();
        }
    }

 

再来看下insert(e)这个方法,它主要是实现notFull和notEmpty之间的交互,就好比生产者生成一个产品后会告诉消费者,你们可以来取产品了:
private void insert(E x) {
       //略
        ++count;
        notEmpty.signal();                 //重点在这里,实现交互
    }

最后,我们看一下take的代码,它的实现跟put是惊人的相似,只是逻辑相反而已:
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            try {
                while (count == 0)
                    notEmpty.await();
            } catch (InterruptedException ie) {
                notEmpty.signal(); // propagate to non-interrupted thread
                throw ie;
            }
            E x = extract();
            return x;
        } finally {
            lock.unlock();
        }
    }

另:源码中涉及到2个条件,条件的概念我也是在这里第一次看到,特意查了下API,发现其实就是具备实际含义的加锁对象,这个很好理解,很多通过notify和wait实现的场景下,都需要配合使用锁,所以就以为着必须有一个对象被加锁,有时候我会用一个list,里面什么都不放,就只是作为互斥加锁的一个承载对象,但是用一个list理解上点怪,比较它只是为了加锁而存在的一个傀儡。所以,jdk引入了Condition,目的就是让锁的承载对象脱离具体的数据结构,且更具备业务上的含义。当然,处于优化考虑,它还是对notify、notifyall和wait做了一些优化和重组的。具体可以参看源码


 

0 0