ArrayBlockingQueue,LinkedBlockingQueue分析

BlockingQueue接口定义了一种阻塞的FIFO queue,每一个BlockingQueue都有一个容量,让容量满时往BlockingQueue中添加数据时会造成阻塞,当容量为空时取元素操作会阻塞。

ArrayBlockingQueue是一个由数组支持的有界阻塞队列。在读写操作上都需要锁住整个容器,因此吞吐量与一般的实现是相似的,适合于实现“生产者消费者”模式。

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

代码:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Main {
    public static void main(String[] args)  {
        final BlockingQueue queue = new ArrayBlockingQueue(3);
        //final BlockingQueue queue = new LinkedBlockingQueue(3);
        for(int i=0;i<2;i++){
            new Thread(){
                public void run(){
                    while(true){
                        try {
                            Thread.sleep((long)(Math.random()*1000));
                            System.out.println(Thread.currentThread().getName() + "准备放数据!");
                            queue.put(1);
                            System.out.println(Thread.currentThread().getName() + "已经放了数据," +
                                    "队列目前有" + queue.size() + "个数据");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                    }
                }

            }.start();
        }

        new Thread(){
            public void run(){
                while(true){
                    try {
                        //将此处的睡眠时间分别改为100和1000,观察运行结果
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName() + "准备取数据!");
                        queue.take();
                        System.out.println(Thread.currentThread().getName() + "已经取走数据," +
                                "队列目前有" + queue.size() + "个数据");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }.start();
    }
}

 

1000:

Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有2个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有1个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有2个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有3个数据
Thread-0准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有2个数据
Thread-0已经放了数据,队列目前有3个数据
Thread-1准备放数据!
Thread-0准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有2个数据
Thread-1已经放了数据,队列目前有3个数据
Thread-1准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有2个数据
Thread-0已经放了数据,队列目前有3个数据
Thread-0准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有2个数据
Thread-1已经放了数据,队列目前有3个数据
Thread-1准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有2个数据
Thread-0已经放了数据,队列目前有3个数据
Thread-0准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有2个数据
Thread-1已经放了数据,队列目前有3个数据
Thread-1准备放数据!
Thread-2准备取数据!
Thread-0已经放了数据,队列目前有3个数据
Thread-2已经取走数据,队列目前有3个数据
Thread-0准备放数据!
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有2个数据
Thread-1已经放了数据,队列目前有3个数据
Thread-1准备放数据!
Thread-2准备取数据!
View Code

100:

Thread-2准备取数据!
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-1准备放数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-1已经放了数据,队列目前有0个数据
Thread-2准备取数据!
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-1准备放数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-1已经放了数据,队列目前有0个数据
Thread-2准备取数据!
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有2个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有1个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有2个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有1个数据
Thread-2准备取数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-0准备放数据!
Thread-2已经取走数据,队列目前有0个数据
Thread-0已经放了数据,队列目前有0个数据
Thread-2准备取数据!
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-2准备取数据!
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-2已经取走数据,队列目前有0个数据
Thread-1准备放数据!
Thread-1已经放了数据,队列目前有1个数据
Thread-0准备放数据!
Thread-0已经放了数据,队列目前有2个数据
Thread-2准备取数据!
View Code

ArrayBlockingQueue与LinkedBlockingQueue的区别:

1.    队列中锁的实现不同
       ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;
       LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock
    
2.    在生产或消费时操作不同
       ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;
       LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node<E>进行插入或移除,会影响性能

3.    队列大小初始化方式不同
       ArrayBlockingQueue实现的队列中必须指定队列的大小;
       LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE
    
注意:
1.    在使用LinkedBlockingQueue时,若用默认大小且当生产速度大于消费速度时候,有可能会内存溢出
2.    在使用ArrayBlockingQueue和LinkedBlockingQueue分别对1000000个简单字符做入队操作时,
       LinkedBlockingQueue的消耗是ArrayBlockingQueue消耗的10倍左右,
       即LinkedBlockingQueue消耗在1500毫秒左右,而ArrayBlockingQueue只需150毫秒左右。

=======================================================

BlockingQueue具体分析:(Java7源码)

add()和remove()是最原始的方法,也是最不常用的。

原因是,当队列满了或者空了的时候,会抛出IllegalStateException("Queuefull")/NoSuchElementException(),并不符合我们对阻塞队列的要求;

因此,ArrayBlockingQueue里,这两个方法的实现,直接继承自java.util.AbstractQueue:

public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

    public E remove() {
        E x = poll();
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();
    }

有上述源码可知,add()和remove()实现的关键,是来自java.util.Queue接口的offer()和poll()方法。
offer():在队列尾插入一个元素。若成功便返回true,若队列已满则返回false。
poll():同理,取出并删除队列头的一个元素。若成功便返回元素,若队列为空则返回null。
这里使用的是ReentrantLock,在插入或者取出前,都必须获得队列的锁,以保证同步。

public boolean offer(E e) {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                insert(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return (count == 0) ? null : extract();
        } finally {
            lock.unlock();
        }
    }

由于offer()/poll()是非阻塞方法,一旦队列已满或者已空,均会马上返回结果,也不能达到阻塞队列的目的。因此有了put()/take()这两个阻塞方法:

public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            insert(e);
        } finally {
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return extract();
        } finally {
            lock.unlock();
        }
    }

put()/take()的实现,比起offer()/poll()复杂了一些,尤其有两个地方值得注意:

1. 取得锁以后,循环判断队列是否已满或者已空,并加上Condition的await()方法将当前正在调用put()的线程挂起,直至notFull.signal()唤起。

2. 这里使用的是lock.lockInterruptibly()而不是lock.lock()。原因在这里。lockInterruptibly()这个方法,优先考虑响应中断,而不是响应普通获得锁或重入获得锁。简单来说就是,由于put()/take()是阻塞方法,一旦有interruption发生,必须马上做出反应,否则可能会一直阻塞。

 

最后,无论是offer()/poll()还是put()/take(),都要靠insert()/extract()这个私有方法去完成真正的工作:

private void insert(E x) {
        items[putIndex] = x;
        putIndex = inc(putIndex);
        ++count;
        notEmpty.signal();
    }

    final int inc(int i) {
        return (++i == items.length) ? 0 : i;
    }

    private E extract() {
        final Object[] items = this.items;
        E x = this.<E>cast(items[takeIndex]);
        items[takeIndex] = null;
        takeIndex = inc(takeIndex);
        --count;
        notFull.signal();
        return x;
    }

    final int dec(int i) {
        return ((i == 0) ? items.length : i) - 1;
    }

insert()/extract(),是真正将元素放进数组或者将元素从数组取出并删除的方法。由于ArrayBlockingQueue是有界限的队列(Bounded Queue),因此inc()/dec()方法保证元素不超出队列的界限。

另外,每当insert()后,要使用notEmpty.signal()唤起因队列空而等待取出的线程;每当extract()后,同理要使用notFull.signal()唤起因队列满而等待插入的线程。

列举一些使用队列时的错误做法:

1. 不能忽略offer()的返回值。offer()作为有返回值的方法,可以在判断的时候十分有作用(例如add()的实现)。因此,千万不要忽略offer()方法的返回值。

2. 在循环里使用isEmpty()和阻塞方法:

    take()是阻塞方法,无需做isEmpty()的判断,直接使用即可。而这种情况很有可能导致死锁,因为由于不断循环,锁会一直被isEmpty()取得(因为size()方法会取得锁),而生产者无法获得锁。

3. 频繁使用size()方法去记录。size()方法是要取得锁的,意味着这不是一个廉价的方法。可以使用原子变量代替。

 

阻塞队列的操作:

add        增加一个元索                     如果队列已满,则抛出一个IIIegaISlabEepeplian异常
remove   移除并返回队列头部的元素    如果队列为空,则抛出一个NoSuchElementException异常
element  返回队列头部的元素             如果队列为空,则抛出一个NoSuchElementException异常
offer       添加一个元素并返回true       如果队列已满,则返回false
poll         移除并返问队列头部的元素    如果队列为空,则返回null
peek       返回队列头部的元素             如果队列为空,则返回null
put         添加一个元素                      如果队列满,则阻塞
take        移除并返回队列头部的元素     如果队列为空,则阻塞

remove、element、offer 、poll、peek 其实是属于Queue接口。 

 

http://www.cnblogs.com/liuling/p/2013-8-20-01.html

http://www.cnblogs.com/techyc/p/3782079.html?utm_source=tuicool&utm_medium=referral

http://blog.csdn.net/luohuacanyue/article/details/16359777

http://jiangzhengjun.iteye.com/blog/683593

posted @ 2016-12-09 17:44  hongdada  阅读(933)  评论(0编辑  收藏  举报