图解LinkedBlockingQueue源码(java 8)

LinkedBlockingQueue是一个阻塞队列,采用双锁技术,性能较 ArrayBlockingQueue有很大的提高。

源码层面,有很多地方与ArrayBlockingQueue原理类似。前面写过一篇ArrayBlockingQueue源码解析, 看这篇文章之前,可以先看下。

		LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
        Thread t0 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    boolean offer = queue.offer(i);
                    log.info("offer:{}, value:{}", offer, i);
                    Thread.sleep(300);
                }
            }
        }, "t0");
        Thread t1 = new Thread(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    Integer take = queue.take();
                    log.info("take:{}", take);
                }
            }
        }, "t1");
       t1.start();
       t0.start();
    }

上面是一个demo,一个线程往队列中放,一个线程从队列中取。当队列为空时,放的操作会阻塞。

下面以这个为例,详细说明其源码。

一、初始化

	LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();

这行代码,可以传一个数字进去,指队列的长度。如果不传,默认是 Integer 的最大值


    public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity; // 队列容量
        last = head = new Node<E>(null); // 初始化节点
    }
    

另外,有必要了解下LinkedBlockingQueue的几个固有属性


    private final int capacity; // 队列容量

    private final AtomicInteger count = new AtomicInteger(); // 当前队列中元素个数

    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 boolean offer(E e) {
        if (e == null) throw new NullPointerException();
        final AtomicInteger count = this.count;
        if (count.get() == capacity) // 队满直接返回false
            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(); // 队列中元素计数加1,并返回原来的个数
                if (c + 1 < capacity)
                    notFull.signal(); // 队满有空位,唤醒其它放元素的线程
            }
        } finally {
            putLock.unlock();
        }
        if (c == 0)
            signalNotEmpty(); // 唤醒出队线程
        return c >= 0;
    }

enqueue(node),这段代码很简单,底层是链表,追加一个就可以了。


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



	// Node 是一个内部类,item记录入队的元素, next 指向下一个节点
    static class Node<E> {
        E item;
        
        Node<E> next;

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

signalNotEmpty() 这个方法,本质是 AQS里的 signal方法,用一句话概括

就是从等待队列中取一个节点,放入CLH队列(抢锁队列)

详细的分析可以我的另一篇博客中查找:signal源码分析


    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
    }

从源码上看,是 c == 0 时调用 signalNotEmpty 这个方法,为什么?

正常情况下,c = count.getAndIncrement(),看仔细了,是入队前队列中元素的个数

c == 0, 即原来队列是空的,现在放了一个进去。

原来空队列时,有可能会有取元素的线程在等待,所以,要唤醒下。

return c >= 0 这个开始我没想明白,什么情况下,c 会等于 -1

if (count.get() < capacity) 当队满时,这个条件为false,直接就释放锁,c == -1,

即往队列中放元素失败

三、出队源码


 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)
                notEmpty.signal(); // 队列不空,通知其它出队线程干活。
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull(); // 通知入队线程,往队列里放
        return x;
    }

这里的 notEmpty.await() 用一句话概括:释放锁,阻塞。当线程被唤醒时,抢锁,直到抢到为止。

notEmpty.signal() 用一句话概括:从等待队列中取出一个节点,放到CLH队列(抢锁队列)

这两个方法,源码解析,可以看我的另外一篇文章:awtit() 与 signal()

出队的最后一段代码,是当原队列是满的时候,此次出队,触发唤醒入队的线程。

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

四、阻塞与唤醒

前面讲入队方法 offer(e),这个方法是无阻塞的,返回 true,入队成功,返回 false,入队失败。
入队方法 put(e),这方法是阻塞的,即队列满时,线程阻塞,直到有队列中有空位时,执行入队。

下面我用几张图来说明,入队与出队时,阻塞与唤醒的具体场景(put–入队, take–出队)

队列的长度设定为3,先有五个线程(t1 - t5)往队列里放元素。队列长度是3,有两个线程会阻塞

在这里插入图片描述

这时,假设 t6线程来取元素,这是队满时取元素,最后会执行signalNotFull()这个方法

    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();
        } finally {
            putLock.unlock();
        }
    }

notFull.signal() 这行代码执行后,就是这么一个场景
在这里插入图片描述

putLock.unlock() 这行代码执行时,会唤醒t4线程(这篇文章中详细说明 解锁源码

t4 是往队列中放的时候,没位置阻塞了。t6取出一个元素后,就有了一个空位,t4被唤醒了,就继续执行,往队列中放。

在 t4 往队列中放的时候,如果还有空位,那还会去唤醒 t5 ,执行放元素的操作。

putLock 与 takeLock 之间在队满或队空的时候,相互之间会进行通讯。

总结

LinkedBlockingQueue 采用双锁,即入队有入队的锁,出队有出队的锁。

入队方法,put() 队满时阻塞, offer() 队满时,直接返回false
出队方法,take() 队空时阻塞,poll() 队空时,返回null

上一篇ArrayBlockingQueue 源码解析
下一篇PriorityBlockingQueue 源码解析

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值