Android之阻塞队列LinkedBlockingQueue使用及源码解析

在一篇分析AsyncTask源码中的文章中,我们看到了在线程并发中用到比较多的一个队列LinkedBlockingQueue,今天这篇文章就来分析下这个东西的使用。

LinkedBlockingQueue:是concurrent包下的类,实现了BlockingQueue接口,是一种单向阻塞链表,是线程安全的队列,数据处理逻辑是先进先出,应该说是作为生产者消费者模型的首选;可以指定最大容量,也可以不指定最大容量,如果不指定的话,默认最大值是Integer.MAX_VALUE。如图

这里写图片描述

线程1不停的往队列尾部加入数据,线程2不停的从头部取出数据。

通过使用这种队列,我们可以放心的实现多线程操作,不需要自己开发,要处理各种对象锁,队列安全问题。

LingkedBlockingQueue有两套增加删除数据的方法

 *  <tr>
 *    <td></td>
 *    <td ALIGN=CENTER><em>Throws exception</em></td>
 *    <td ALIGN=CENTER><em>Special value</em></td>
 *    <td ALIGN=CENTER><em>Blocks</em></td>
 *    <td ALIGN=CENTER><em>Times out</em></td>
 *  </tr>
 *  <tr>
 *    <td><b>Insert</b></td>
 *    <td>{@link #add add(e)}</td>
 *    <td>{@link #offer offer(e)}</td>
 *    <td>{@link #put put(e)}</td>
 *    <td>{@link #offer(Object, long, TimeUnit) offer(e, time, unit)}</td>
 *  </tr>
 *  <tr>
 *    <td><b>Remove</b></td>
 *    <td>{@link #remove remove()}</td>
 *    <td>{@link #poll poll()}</td>
 *    <td>{@link #take take()}</td>
 *    <td>{@link #poll(long, TimeUnit) poll(time, unit)}</td>
 *  </t>

现在我们通过代码来看看这两套方法如何操作。本例使用生产者消费者模型,构建一个苹果消费者,一个苹果生产者,一个装苹果的篮子,一个苹果对象。

苹果生产者

/**
 * @Description TODO(苹果生产者)
 * @author cxy
 * @Date 2018/6/21 17:13
 */
public class Producer implements Runnable{

    private String TAG = "Producer";

    //生产者
    private String produce;
    //苹果篮子
    private AppleBasket basket;

    public Producer(String produce, AppleBasket basket) {
        this.produce = produce;
        this.basket = basket;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; ; i++) {
                Apple bean = new Apple("苹果" + i + "号");
                Log.e(TAG, produce + "正在生产苹果 " + bean);
                basket.produce(bean, produce);
                Log.e(TAG, produce + "生产苹果 " + bean + " 结束----------------");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            Log.e(TAG, "InterruptedException=" + e.getMessage());
            e.printStackTrace();
        } catch (IllegalStateException e) {
            Log.e(TAG, "IllegalStateException=" + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            Log.e(TAG, "Exception=" + e.getMessage());
            e.printStackTrace();
        }
    }
}

苹果消费者

/**
 * @Description TODO(苹果消费者)
 * @author cxy
 * @Date 2018/6/21 17:14
 */
public class Consumer implements Runnable {

    private String TAG = "Consumer";

    private String consume;
    private AppleBasket basket;

    public Consumer(String consume, AppleBasket basket) {
        this.consume = consume;
        this.basket = basket;
    }

    @Override
    public void run() {

        try {
            while (true) {
                Log.e(TAG,consume+"正在消费苹果 ");
                basket.consume(consume);
                Log.e(TAG,consume+"消费苹果结束 -----------------------");
                Thread.sleep(1000 * 20);
            }
        } catch (InterruptedException e) {
            Log.e(TAG, "InterruptedException=" + e.getMessage());
            e.printStackTrace();
        } catch (IllegalStateException e) {
            Log.e(TAG, "IllegalStateException=" + e.getMessage());
            e.printStackTrace();
        } catch (Exception e) {
            Log.e(TAG, "Exception=" + e.getMessage());
            e.printStackTrace();
        }

    }


}

苹果篮子

/**
 * @Description TODO(装苹果的篮子)
 * @author cxy
 * @Date 2018/6/21 17:03
 */
public class AppleBasket {

    private String TAG = "AppleBasket";

    BlockingQueue<Apple> queue = new LinkedBlockingQueue<>(10);

    /**
     * 生成苹果,放入队列
     * @param bean
     * @throws InterruptedException
     */
    public void produce(Apple bean,String producer) throws InterruptedException {
        //添加元素到队列,如果队列已满,线程进入等待,直到有空间继续生产
        queue.put(bean);
        //添加元素到队列,如果队列已满,抛出IllegalStateException异常,退出生产模式
//        queue.add(bean);
        //添加元素到队列,如果队列已满或者说添加失败,返回false,否则返回true,继续生产
//        queue.offer(bean);
        //添加元素到队列,如果队列已满,就等待指定时间,如果添加成功就返回true,否则false,继续生产
//        queue.offer(bean,5, TimeUnit.SECONDS);
    }

    /**
     * 消费苹果。从队列取出
     * @return
     * @throws InterruptedException
     */
    public Apple consume(String consumer) throws InterruptedException {
        //检索并移除队列头部元素,如果队列为空,线程进入等待,直到有新的数据加入继续消费
        Apple bean = queue.take();
        //检索并删除队列头部元素,如果队列为空,抛出异常,退出消费模式
//        Apple bean = queue.remove();
        //检索并删除队列头部元素,如果队列为空,返回false,否则返回true,继续消费
//        Apple bean = queue.poll();
        //检索并删除队列头部元素,如果队列为空,则等待指定时间,成功返回true,否则返回false,继续消费
//        Apple bean = queue.poll(3, TimeUnit.SECONDS);
        return bean;
    }
}

苹果对象

/**
 * @Description TODO(苹果类)
 * @author cxy
 * @Date 2018/6/21 17:04
 */
public class Apple {

    private String name;

    public Apple(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return  name ;
    }
}

现在来使用吧,在Activity里使用

 AppleBasket basket = new AppleBasket();
        ExecutorService service = Executors.newFixedThreadPool(5);
        Producer producer = new Producer("生产者",basket);
        Consumer consumer = new Consumer("消费者",basket);
        service.execute(producer);
        service.execute(consumer);

上面代码逻辑就是生产者每过一秒生产一个苹果,篮子采用put方法往队列里放苹果;消费者每过20秒消费一个苹果,篮子采用take方法从篮子取数据,队列的容量是只能放10个苹果;这样的情况就是当生产者生产了10个苹果以后,篮子就满了,但是此时消费者还没有开始消费苹果,那么这时候生产者就会阻塞住,也就是生产线程被wait了,一直到20s后消费者开始消费苹果了,这时候生产线程就被唤醒了,就继续开始生产苹果,知道队列满了再继续被阻塞。我们看打印的日志

06-22 10:39:46.778 25997-26045/? E/Producer: 生产者正在生产苹果 苹果006-22 10:39:46.778 25997-26045/? E/Producer: 生产者生产苹果 苹果0号 结束----------------
06-22 10:39:47.778 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果106-22 10:39:47.779 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果1号 结束----------------
06-22 10:39:48.779 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果206-22 10:39:48.779 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果2号 结束----------------
06-22 10:39:49.779 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果306-22 10:39:49.779 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果3号 结束----------------
06-22 10:39:50.779 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果406-22 10:39:50.780 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果4号 结束----------------
06-22 10:39:51.780 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果506-22 10:39:51.780 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果5号 结束----------------
06-22 10:39:52.780 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果606-22 10:39:52.780 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果6号 结束----------------
06-22 10:39:53.781 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果706-22 10:39:53.781 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果7号 结束----------------
06-22 10:39:54.781 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果806-22 10:39:54.781 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果8号 结束----------------
06-22 10:39:55.781 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果906-22 10:39:55.782 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果9号 结束----------------
06-22 10:39:56.782 25997-26045/com.android.mangodialog E/Producer: 生产者正在生产苹果 苹果1006-22 10:40:06.778 25997-26046/com.android.mangodialog E/Consumer: 消费者正在消费苹果 
06-22 10:40:06.778 25997-26046/com.android.mangodialog E/Consumer: 消费者消费苹果结束 -----------------------
06-22 10:40:06.778 25997-26045/com.android.mangodialog E/Producer: 生产者生产苹果 苹果10号 结束----------------

当生产者生产到第10号苹果的时候,队列已经放了10个苹果,这时候线程就被阻塞了,直到消费者消费了一个苹果以后,生产者才生产成功了一个苹果。

如果我们把逻辑改下,生产者每20s生产一个苹果,消费者每过1s消费一个苹果,那么情况就是消费者就会被堵塞住,当20s的时候生产者生产了一个苹果,消费者就开始成功消费一个苹果,然后就继续被阻塞了,知道下一个苹果被成功生产。

我们看看LinkedBlockingQueue的源码,前面说了它是一个单向链表阻塞队列,看看源码中的节点类

/** 
 * Linked list node class. 
 */  
static class Node<E> {  
    E item;  

    /** 
     * One of: 
     * - the real successor Node 
     * - this Node, meaning the successor is head.next 
     * - null, meaning there is no successor (this is the last node) 
     */  
    Node<E> next;  

    Node(E x) { item = x; }  
} 

节点类只维护了后一个节点,没有前一个节点(后面讲到LinkedBlockingDeque时会讲到两个节点),这就是它的单向链表由来。

再看看这个类里面定义的变量

    /** 链表容量,如果没有指定,设置为Integer.MAX_VALUE */
    private final int capacity;

    /** 链表当前元素数量 */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * Head of linked list.
     * Invariant: head.item == null
     * 队列头部节点,并且头部节点中的元素为null
     */
    transient Node<E> head;

    /**
     * Tail of linked list.
     * Invariant: last.next == null
     * 队列尾部节点,并且尾部节点后面的一个节点为null
     */
    private transient Node<E> last;

    /** Lock held by take, poll, etc
     * 用于take、poll等方法将元素出队的锁
     * */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** Wait queue for waiting takes
     * 当队列为空时,保存出队线程
     * */
    private final Condition notEmpty = takeLock.newCondition();

    /** Lock held by put, offer, etc
     * 用于put、offer等方法将元素入队的锁
     * */
    private final ReentrantLock putLock = new ReentrantLock();

    /** Wait queue for waiting puts
     *  当队列满的时候,保存入队的线程
     * */
    private final Condition notFull = putLock.newCondition();

看到这里定义了两把锁,一把控制出队线程,一把控制入队线程,这说明了同一时间只能有一个线程出队,一个线程入队,其余线程都会被阻塞;并且用了AtomicInteger 来表示队列元素个数,这是原子操作的Integer,也就是每次都是来读取这个真正的内存中的值,而不是去读缓存,也就能保证出队线程和入队线程在操作队列时是安全的。

再看接下来构造方法

/**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}.
     * 空的构造方法,默认设置队列长度为Integer.MAX_VALUE
     */
    public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
     *
     * @param capacity the capacity of this queue
     * @throws IllegalArgumentException if {@code capacity} is not greater than zero
     *
     * 传入一个队列长度值,并且实例化了一个头部节点和尾部节点,且节点内的元素为null
     */
    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node<E>(null);
    }

    /**
     * Creates a {@code LinkedBlockingQueue} with a capacity of
     * {@link Integer#MAX_VALUE}, initially containing the elements of the
     * given collection,
     * added in traversal order of the collection's iterator.
     *
     * @param c the collection of elements to initially contain
     * @throws NullPointerException if the specified collection or any
     *         of its elements are null
     *
     *  传入一个集合
     */
    public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        // Never contended, but necessary for visibility
        //获取队列入队锁,虽然这时候没人竞争锁,但是有必要锁住
        putLock.lock();
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                //将元素入队
                enqueue(new Node<E>(e));
                //将元素计数
                ++n;
            }
            //设置队列长度
            count.set(n);
        } finally {
            //释放锁
            putLock.unlock();
        }
    }

这里有一个必走的构造方法就是第二个,做了两件事,指定队列长度,实例化一个头部节点和一个尾部节点,不过节点内的元素为null。

看看put操作和take操作

public void put(E e) throws InterruptedException {
        if (e == null) throw new NullPointerException();
        // Note: convention in all put/take/etc is to preset local var
        // holding count negative to indicate failure unless set.
        int c = -1;
        //以e创建节点
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        //获取入队锁
        putLock.lockInterruptibly();
        try {
            /*
             * Note that count is used in wait guard even though it is
             * not protected by lock. This works because count can
             * only decrease at this point (all other puts are shut
             * out by lock), and we (or some other waiting put) are
             * signalled if it ever changes from capacity. Similarly
             * for all other uses of count in other wait guards.
             * 如果队列满了,就将线程放入到Condition等待队列中
             */
            while (count.get() == capacity) {
                notFull.await();
            }
            //将节点放入到队列中
            enqueue(node);
            //获取当前队列长度,然后将count加一
            c = count.getAndIncrement();
            //如果队列没有满,就通知其它的入队线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            //释放入队锁
            putLock.unlock();
        }
        //如果队列长度从0到非0,那就通知出队线程来取数据
        if (c == 0)
            signalNotEmpty();
    }
public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        //获取出队锁
        takeLock.lockInterruptibly();
        try {
            //如果队列是空的,就将线程放入到Condition等待队列中
            while (count.get() == 0) {
                notEmpty.await();
            }
            //取出一个节点元素
            x = dequeue();
            //获取取出之前的队列长度
            c = count.getAndDecrement();
            //如果队列不为空,通知Condition等待队列中的线程来取数据
            if (c > 1)
                notEmpty.signal();
        } finally {
            //释放锁
            takeLock.unlock();
        }
        //如果队列长度从满到非满,通知入队线程生产数据
        if (c == capacity)
            signalNotFull();
        return x;
    }
/**
     * Signals a waiting take. Called only from put/offer (which do not
     * otherwise ordinarily lock takeLock.)
     * 通知出队线程,队列已经不为空了,可以来获取元素了
     */
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        //获取元素出队的锁
        takeLock.lock();
        try {
            //释放出队线程的第一个等待线程
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

    /**
     * Signals a waiting put. Called only from take/poll.
     * 通知入队线程,队列没有满,可以来添加元素了
     */
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        //获取元素入队的锁
        putLock.lock();
        try {
            //释放入队线程的第一个线程
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

/**
     * Links node at end of queue.
     *  创建一个节点,添加到链表尾部
     * @param node the node
     */
    private void enqueue(Node<E> node) {
        // assert putLock.isHeldByCurrentThread();
        // assert last.next == null;
        //封装新节点,并赋给当前的最后一个节点的下一个节点,然后将这个节点设为最后一个节点
        last = last.next = node;
    }

    /**
     * Removes a node from head of queue.
     * 从队列头部移除一个节点
     * @return the node
     */
    private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        //获取头节点 元素为null
        Node<E> h = head;
        //将头节点的下一个节点赋值给first
        Node<E> first = h.next;
        //将当前将要出队的节点置为null,为了使其做head节点做准备
        h.next = h; // help GC
        //将当前要出队的节点作为头节点
        head = first;
        //获取出队节点的元素值
        E x = first.item;
        //将出队节点的元素值置为null
        first.item = null;
        return x;
    }

注释描述的很清楚了,其实最主要的是要理解出队dequeue和入队enqueue两个方法的逻辑

首先我们来看下节点类中的next的这段注释
/**
* One of:
* - the real successor Node
* - this Node, meaning the successor is head.next
* - null, meaning there is no successor (this is the last node)
*/
Node next;

也就是说当前节点的下一个节点才是我们真正去操作的节点,如果这个节点的下一个节点为null,那这个节点就是最后一个节点;

这里写图片描述

这可以看出head.item=null,last.next = null

还有一个意思其实是head这个节点从初始化的时候就能看出内部元素为null,如下图

这里写图片描述

当我们调用构造方法的时候,内部有这么一句代码
last = head = new Node(null);
就是将last引用和head引用都指向了这个new的节点,元素为null

接下来就是我们调用put方法添加节点的时候了,从上面代码可以看出,最终会走到enqueue这个方法,就一句代码
last = last.next = node;
先将新封装的节点引用赋值给了last.next;但是这时候last的引用还是和head一样指向第一个节点(这时候的last.next和head.next是一样的),所以这时候第二个等号将last.next的引用又赋值给了last
这里写图片描述

然后调用take方法出队,看看出队逻辑
//获取头节点 元素为null
Node h = head;
//将头节点的下一个节点赋值给first
Node first = h.next;
//将当前将要出队的节点指到head节点的位置
h.next = h; // help GC
//将当前要出队的节点作为头节点
head = first;
//获取出队节点的元素值
E x = first.item;
//将出队节点的元素值置为null
first.item = null;
return x;

执行完第一句和第二句代码后
这里写图片描述

逻辑就是:将头节点的引用给到了h,将新增加的节点的引用给到了first,接下来执行最后的代码

这里写图片描述
第三步:
将第二个节点也就是要出队的节点指向head节点,
第四步:
将当前要出对的节点引用赋值给head;这一步走完,存在堆内存中的第一个节点就没有全局变量的引用指向它
第五步:
把要出队的节点的元素取出来赋值给x
第五步:
将第二个节点元素置为null,然后将元素返回

当这个方法执行完毕,最开始的第一个节点没有全局变量引用指向它,方法内的引用会从栈内回收,第一个节点的堆内存最终也会被回收,而第二个节点中的元素置为null,就这样它变成了第一个节点,同样的只有两个全局变量的引用head和last指向它。

这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq_30993595/article/details/80770474