LinkedBlockingQueue源码分析(JDK8)

来源:互联网 发布:布尔人 知乎 编辑:程序博客网 时间:2024/05/16 03:51
本文分析LinkedBlockingQueue的源码,学习其底层实现。
你可以通过我的这篇文章《阻塞队列BlockingQueue(JDK8)》了解阻塞队列的基本使用及各个方法的对比分析。
本文链接:http://blog.csdn.net/u010887744/article/details/73010691
LinkedBlockingQueue特性及使用场景:
  1. 基于链表实现,线程安全的阻塞队列。
  2. “two lock queue”算法变体,双锁(ReentrantLock):takeLock、putLock,允许读写并行,remove(e)和迭代器iterators需要获取2个锁。
  3. FIFO先进先出模式。
  4. 在大部分并发场景下,LinkedBlockingQueue的吞吐量比ArrayBlockingQueue更好。
LinkedBlockingQueue应注意:
1、LinkedBlockingQueue默认为无界队列,即大小为Integer.MAX_VALUE,如果消费者速度慢于生产者速度,可能造成内存空间不足,建议手动设置队列大小。
概述LinkedBlockingQueue:
  1. 队列头head是存在于队列中最久的元素,队尾tail是最新加入队列的元素。
  2. 每次插入操作都将动态构造Linked nodes。
  3. publicclass LinkedBlockingQueue<E>extendsAbstractQueue<E>implements BlockingQueue<E>, java.io.Serializable。
  4. AbstractQueue:提供Queue必须实现的方法,add(e)、remove()、element()、clear()、addAll(c)
1、LinkedBlockingQueue重要属性
  • private final intcapacity:队列容量,创建时指定,默认Integer.MAX_VALUE。
  • private final AtomicIntegercount= new AtomicInteger():队列实际元素个数,size()方法返回count值。
  • 【重要】private finalReentrantLocktakeLock= new ReentrantLock():控制取数据的锁putLock:控制存放数据的锁。
  • 【重要】private finalConditionnotEmpty= takeLock.newCondition():出队条件(takeLock锁的条件),队列为空时,通过此Condition让获取元素的线程等待notFull:入队条件(putLock)。
  • static class Node<E>:链表Node节点
static class Node<E> {    E item; // 数据    Node<E> next; // 下一个节点的指针,默认为null意味为最后一个节点,无后继    Node(E x) { item = x; }}
2、LinkedBlockingQueue构造函数
  • LinkedBlockingQueue():初始化容量为Integer.MAX_VALUE的队列;
  • LinkedBlockingQueue(int capacity):指定队列容量并初始化头尾节点,if (capacity <= 0) throw new IllegalArgumentException();last = head = new Node<E>(null)。
  • LinkedBlockingQueue(Collection<? extends E> c):初始化一个容量为Integer.MAX_VALUE且包含集合c所有元素的队列,且阻塞队列的迭代顺序同集合c。若集合c元素包含null,将throwNullPointerException;若集合c元素个数达到Integer.MAX_VALUE,将throwIllegalStateException("Queue full")。


// 将node链接到队列尾部private void enqueue(Node<E> node) { // 入队    // assert putLock.isHeldByCurrentThread();    // assert last.next == null;    last = last.next = node; // 等价于last.next = node;last = last.next(即node)}public LinkedBlockingQueue(Collection<? extends E> c) {        this(Integer.MAX_VALUE);    final ReentrantLock putLock = this.putLock;    putLock.lock(); // Never contended(竞争), but necessary for visibility(可见性)    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)); // 执行last = last.next = node;            ++n;        }        count.set(n); // 设置队列元素个数    } finally {        putLock.unlock();    }}


3、添加方法
先看几个重要方法:
/*** 唤醒notEmpty上正在等待获取元素的线程*/private void signalNotEmpty() {    final ReentrantLock takeLock = this.takeLock;    takeLock.lock();    try {        notEmpty.signal();    } finally {        takeLock.unlock();    }}
3.1、offer(e)
// 队列已满:falsepublic boolean offer(E e) {    if (e == null) throw new NullPointerException();    final AtomicInteger count = this.count;    if (count.get() == capacity) // 队列容量达到最大值,添加失败        return false;    int c = -1;    Node<E> node = new Node<E>(e);    final ReentrantLock putLock = this.putLock;    putLock.lock(); // 获取插入锁putLock    try {        if (count.get() < capacity) { // 加锁后再次判断队列是否已满            enqueue(node); // 入队            c = count.getAndIncrement(); // 返回Inc之前的值            if (c + 1 < capacity) // 插入节点后队列未满                notFull.signal(); // 唤醒notFull上的等待线程        }    } finally {        putLock.unlock(); // 释放插入锁    }    if (c == 0)        signalNotEmpty(); // 如果offer前队列为空,则唤醒notEmpty上的等待线程    return c >= 0;}

衍生方法:
public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException {}
此方法和offer(E e)代码和功能均相似,但是如果在指定时间内未插入成功则会返回false。
比offer(E e)多的部分代码分析:
long nanos = unit.toNanos(timeout);  //将指定的时间长度转换为毫秒来进行处理while (count.get() == capacity) {    if (nanos <= 0) // 等待的剩余时间小于等于0,那么直接返回false        return false;    nanos = notFull.awaitNanos(nanos); // 最多等待时间(纳秒)}
long awaitNanos(long nanosTimeout) throws InterruptedException;
指定时间内收到signal()或signalALL()则返回nanosTimeout减去已经等待的时间;若指定时间内有其它线程中断该线程,则抛出InterruptedException并清除当前线程的打断状态;若指定时间内未收到通知,则返回0或负数。
3.2、add(e)
调用AbstractQueue的add(E e)方法。
public boolean add(E e) {    if (offer(e))        return true;    else        throw new IllegalStateException("Queue full");}

衍生方法:
booleanaddAll(Collection<? extends E> c):(属于AbstractQueue类)循环调用add(E e)方法,内部结构为foreach遍历add(e),所以可能部分数据插入成功,部分插入失败。
3.3、put(e)
// 一直阻塞直到插入成功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;    Node<E> node = new Node<E>(e);    final ReentrantLock putLock = this.putLock;    final AtomicInteger count = this.count; // 可中断的锁获取操作(优先考虑响应中断),如果线程由于获取锁而处于Blocked状态时,线程将被中断而不再继续等待(throws InterruptedException),可避免死锁。    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.         */// 队列若满线程将处于等待状态。while循环可避免“伪唤醒”(线程被唤醒时队列大小依旧达到最大值)        while (count.get() == capacity) {            notFull.await(); // notFull:入队条件        }        enqueue(node); // 将node链接到队列尾部        c = count.getAndIncrement(); // 元素入队后队列元素总和        if (c + 1 < capacity) // 队列未满            notFull.signal(); // 唤醒其他执行入队列的线程    } finally {        putLock.unlock(); // 释放锁    }// c=0说明队列之前为空,出队列线程均处于等待状态。添加一个元素后,队列已不为空,于是唤醒等待获取元素的线程    if (c == 0)        signalNotEmpty();}

Note:lock 与 lockInterruptibly区别
lock优先考虑获取锁,待获取锁成功后,才响应中断。
lockInterruptibly 优先考虑响应中断,而不是响应锁的普通获取或重入获取。
详细区别:
ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态,然后再中断线程。

LinkedBlockingQueue插入数据小结:
线程A入队操作前会获取putLock锁,插入数据完毕后释放;
队列未满将新建Node节点,添加到队列末尾;
队列已满则阻塞线程(notFull.await())或返回false;若线程B取出数据,则会调用notFull.signal()唤醒notFull上的等待线程(线程A继续插数据)。
若入队前队列为空,则唤醒notEmpty上等待的获取数据的线程。

4、获取方法
先看几个重要方法:
/** * 唤醒等待插入数据的线程. Called only from take/poll. */private void signalNotFull() {    final ReentrantLock putLock = this.putLock;    putLock.lock();    try {        notFull.signal();    } finally {        putLock.unlock();    }}/*** 队列头部元素出队.** @return the node*/private E dequeue() {    // assert takeLock.isHeldByCurrentThread();    // assert head.item == null;    Node<E> h = head; // 临时变量h    Node<E> first = h.next;    h.next = h; // 形成环引用help GC    head = first;    E x = first.item;    first.item = null;    return x;}

4.1、poll()
// 队列为空返回null而不是抛异常public E poll() {    final AtomicInteger count = this.count;    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(); // 减1并返回旧值            if (c > 1)                notEmpty.signal(); // 唤醒其他取数据的线程        }    } finally {        takeLock.unlock();    } // c等于capacity说明poll之前队列已满,poll一个元素后便可唤醒其他等待插入数据的线程    if (c == capacity)        signalNotFull();    return x;}

衍生方法:
// 为poll方法增加了时间限制,指定时间未取回数据则返回null
public E poll(long timeout, TimeUnit unit)throws InterruptedException{}

4.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();        if (c > 1) // take数据前队列大小大于1,则take后队列至少还有1个元素            notEmpty.signal(); // 唤醒其他取数据的线程    } finally {        takeLock.unlock();    }    if (c == capacity)        signalNotFull(); //唤醒其他等待插入数据的线程    return x;}

4.3、drainTo(Collection<? super E> c, int maxElements)
// 移除最多maxElements个元素并将其加入集合
public int drainTo(Collection<? super E> c, int maxElements) {    if (c == null)        throw new NullPointerException();    if (c == this)        throw new IllegalArgumentException();    if (maxElements <= 0)        return 0;    boolean signalNotFull = false;    final ReentrantLock takeLock = this.takeLock;    takeLock.lock();    try {        int n = Math.min(maxElements, count.get());//转移元素数量不能超过队列总量         // count.get provides visibility to first n Nodes        Node<E> h = head;        int i = 0;        try {            while (i < n) {                Node<E> p = h.next;//从队首获取元素                c.add(p.item);                p.item = null;//p为临时变量,置null方便GC                h.next = h;                h = p;                ++i;            }            return n;        } finally {            // Restore invariants even if c.add() threw            if (i > 0) { // 有数据被转移到集合c中                // assert h.item == null;                head = h; //如果转移前的队列大小等于队列容量,则说明现在队列未满 // 更新count为队列实际大小(减去i得到)                signalNotFull = (count.getAndAdd(-i) == capacity);            }        }    } finally {        takeLock.unlock();        if (signalNotFull)            signalNotFull(); // 唤醒其他等待插入数据的线程    }}

衍生方法:
// 将[所有]可用元素加入集合c
 public int drainTo(Collection<? super E> c) {    return drainTo(c, Integer.MAX_VALUE);}

4.4、boolean retainAll(Collection<?> c)
// 仅保留集合c中包含的元素,队列因此请求而改变则返回true
public boolean retainAll(Collection<?> c) {    Objects.requireNonNull(c); // 集合为null则throw NPE    boolean modified = false;    Iterator<E> it = iterator();    while (it.hasNext()) {        if (!c.contains(it.next())) {            it.remove();            modified = true; // 队列因此请求而改变则返回true        }    }    return modified;}

LinkedBlockingQueue取数据小结:
线程A取数据前会获取takeLock锁,取完数据后释放锁。
队列有数据则(通常)返回队首数据;
若队列为空,则阻塞线程(notEmpty.await())或返回null等;当线程B插入数据后,会调用notEmpty.signal()唤醒notEmpty上的等待线程(线程A继续取数据)。
若取数据前队列已满,则通过notFull.signal()唤醒notFull上等待插入数据的线程。

5、检测方法(取回但不移除)
5.1、E peek()
// 返回队列头,队列为空返回null
public E peek() {    if (count.get() == 0)        return null;    final ReentrantLock takeLock = this.takeLock;    takeLock.lock();    try {        Node<E> first = head.next;        if (first == null)            return null;        else            return first.item;    } finally {        takeLock.unlock();    }}

6、综述
6.1、LinkedBlockingQueue通过对 插入、取出数据 使用不同的锁,实现多线程对竞争资源的互斥访问

6.2、(之前队列为空)添加数据后调用signalNotEmpty()方法唤醒等待取数据的线程;(之前队列已满)取数据后调用signalNotFull()唤醒等待插入数据的线程。这种唤醒模式可节省线程等待时间。

6.3、个别操作需要调用方法fullyLock()同时获取putLock、takeLock两把锁(如方法:clear()、contains(Object o)、remove(Object o)、toArray()、toArray(T[] a)、toString()),注意fullyLock和fullyUnlock获取锁和解锁的顺序刚好相反,避免死锁。
/** * Locks to prevent both puts and takes. */void fullyLock() {    putLock.lock();    takeLock.lock();}/** * Unlocks to allow both puts and takes. */void fullyUnlock() {    takeLock.unlock();    putLock.unlock();}

6.4、线程唤醒signal()
值得注意的是,对notEmpty和notFull的唤醒操作均使用的是signal()而不是signalAll()。
signalAll() 虽然能唤醒Condition上所有等待的线程,但却并不见得会节省资源,相反,唤醒操作会带来上下文切换,且会有锁的竞争。此外,由于此处获取的锁均是同一个(putLock或takeLock),同一时刻被锁的线程只有一个,也就无从谈起唤醒多个线程了。

6.5、LinkedBlockingQueue与ArrayBlockingQueue简要比较
ArrayBlockingQueue底层基于数组,创建时必须指定队列大小,“有界”;LinkedBlockingQueue“无界”,节点动态创建,节点出队后可被GC,故伸缩性较好;
ArrayBlockingQueue入队和出队使用同一个lock(但数据读写操作已非常简洁),读取和写入操作无法并行,LinkedBlockingQueue使用双锁可并行读写,其吞吐量更高。
ArrayBlockingQueue在插入或删除元素时直接放入数组指定位置(putIndex、takeIndex),不会产生或销毁任何额外的对象实例;而LinkedBlockingQueue则会生成一个额外的Node对象,在高效并发处理大量数据时,对GC的影响存在一定的区别。

欢迎个人转载,但须在文章页面明显位置给出原文连接;未经作者同意必须保留此段声明、不得随意修改原文、不得用于商业用途,否则保留追究法律责任的权利。【 CSDN 】:csdn.zxiaofan.com【GitHub】:github.zxiaofan.com如有任何问题,欢迎留言。祝君好运!Life is all about choices! 将来的你一定会感激现在拼命的自己!
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 在商场买到假货怎么办 网上买到假手机怎么办 网上买了假手机怎么办 网银转账被骗了怎么办 支付宝被骗了钱怎么办 被支付宝骗了钱怎么办 头发出油怎么办小妙招 照相的时候脸歪怎么办 怀孕两个月同床了怎么办 小姐被警察抓了怎么办 我的世界迷路了怎么办 砸到手指甲很痛怎么办 手被锤子砸肿了怎么办 拇指突然疼肿了怎么办 手指甲压紫了痛怎么办 砸到手指头肿了怎么办 小婴儿体重不长怎么办 2岁半宝宝不说话怎么办 米兰叶子掉光了怎么办 qq雷霆战机闪退怎么办 微信限额20万怎么办 欧拉方程x<0怎么办 柿子核吃下去了怎么办 橡胶底白色变黄怎么办 20了丁丁还很小怎么办 5角硬币吞进肚子怎么办 孩子吞了5角硬币怎么办 左腿比右腿微粗怎么办 一个腿长一个腿短怎么办 刚买的毛笔很硬怎么办 我哥哥太爱我了怎么办 如果婚姻无法维持该怎么办 捡了一只流浪狗怎么办 微博加载不出来怎么办 我的脾气很暴躁怎么办 歌单恢复已删除怎么办 家里破产了我该怎么办 对付有心计的人怎么办 上嘴唇无故肿了怎么办 下嘴唇肿了一半怎么办 狗狗早上叫不停怎么办