LinkedBlockingQueue

LinkedBlockingQueue 分析

1. 属性分析

LinkedBlockingQueue 是一个FIFO的队列。采用了两个锁,一个take锁(头锁),一个put锁(尾锁),为了尽可能的减少获取锁的操作,count(元素的数量)采用的AtomicInteger。采用双向链表。并且节点里面不支持null。头结点和尾节点在初始化的时候节点里面存放的都是null,但是之后只要有插入,尾节点就不是null了,而是指向链表的最后一个元素。


    // 容量,默认是 Integer.MAX_VALUE
    private final int capacity;

     // 元素数量,
    //这里为啥要用AtomicInteger,而不是用int 类型的。
   // LinkedBlockingQueue采用两个锁,一个take,一个put,如果采用int类型,在计算size的时候就要同时拿到两个锁。
  // 这影响吞吐量。
    private final AtomicInteger count = new AtomicInteger();

    // 头结点
    transient Node<E> head;

    // 尾节点
    private transient Node<E> last;

    // take锁
    private final ReentrantLock takeLock = new ReentrantLock();

     // take的锁的Condition
    private final Condition notEmpty = takeLock.newCondition();

    // put锁
    private final ReentrantLock putLock = new ReentrantLock();

    // put锁的Condition
    private final Condition notFull = putLock.newCondition();

2. put分析

put操作和Offer这些大差不差,这里就分析一个Put方法。

检查非空,构建存储节点,获取putlock(尾锁),如果容量和大小相等,说明队里已经满了,put线程在put锁的Condition里面等待。

如果没有,就入队(尾插法),count++。如果队列没有慢,就唤醒还在等待的put线程继续put(唤醒一个)。释放锁。本次添加之前,队列的数量是不是=0,如果=0,就唤醒take的线程。(获取take锁,唤醒take锁的Condition等待的线程)。

提示:
  1. count.getAndIncrement()

    count.getAndIncrement() 在语义上相当于 count++。如果count=2, c = count++, count和c等于多少?

    count等于3,c等于2。

    理解这个对下面的唤醒take线程有帮助

  2. 为什么在c==0的时候要唤醒take线程

    因为c表示的是这次put操作之前元素的数量,c==0说明,这个时刻可能有take线程在等待。所以就需要唤醒,因为put表示存放了一个,就要唤醒take线程来获取值。

    同样的在take方法里面也有c==capacity的时候,要唤醒put线程一样。(因为c表示的是这次take操作之前元素的数量,take表示出队,c==capacity,这个时刻可能有put线程在等待。所以就需要唤醒,因为出队了一个元素,所以,队列中有空余的容量了)。

 public void put(E e) throws InterruptedException {
           // 检查非空
        if (e == null) throw new NullPointerException();
    
        int c = -1;
    // 构建新节点
        Node<E> node = new Node<E>(e);
    //拿到put锁。
        final ReentrantLock putLock = this.putLock;
   // 注意count是被final修饰的。也就是说,不能改变引用关系,并且能保证内存一致性
        final AtomicInteger count = this.count;
   // 注意,这个上锁是可以中断的
        putLock.lockInterruptibly();
        try {
            //如果元素的数量和容量一致的话,就说明,队列满了,put线程就应该等待,在notFull里面等待。
            // 之后如果与线程唤醒了notFull里面的,继续走while判断,条件不满足就继续往下走,满足就继续wait。
            while (count.get() == capacity) {
                // 这里应该是唤醒消费者来消费,都到容量了。
                notFull.await();
            }
           //入队
            enqueue(node);
          // count.getAndIncrement() 相当于 i++; 下面的就可以等价为
         // c = count++; 
         // c = ?,c=count++之前的值,看懂这个下面的c==0的判断就很好理解了
            c = count.getAndIncrement();
           // 因为c是count++之前的数量,所以,这里要再次+1,判断一下,这里为啥不直接在count.get呢?是不是觉得int直接操作比较快。
            if (c + 1 < capacity)
                notFull.signal(); //(这里是唤醒一个)
        } finally {
           // 释放锁
            putLock.unlock();
        }
       // 如果c==0,说明,一个问题,
       // 在这一次添加元素之前,没有元素了。那就说明这个时刻,可能有consumer是等待的。所以,要唤醒consumer
        if (c == 0)
            signalNotEmpty();
    }

enqueue(入队)

将构建好的元素添加到链表的末尾,并且last赋值给新添加的元素,尾插法。

// 入队,将新构建的元素添加在last的末尾
// 这里写的还是比较巧妙的。画个图看一下
private void enqueue(Node<E> node) {
    // assert putLock.isHeldByCurrentThread();
    // assert last.next == null;
    last = last.next = node;
}
  1. 队列初始化

在这里插入图片描述

  1. 入队

    按照上面入队代码的逻辑就是这个样子,上面的写法是很简单,并且很简洁,很厉害

    但要注意,这个时候头结点里面element还是null。

在这里插入图片描述

3. take分析

take操作和put操作差不多,获取的锁不一样,并且判断的条件不一样。


    public E take() throws InterruptedException {
        // 返回的元素
        E x;
        int c = -1;
        //获取数量,并且这里是final的
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
      
        // 获取take锁(头),并且可以是中断的
        takeLock.lockInterruptibly();
        try {
           // 和get一样,count=0的时候,说明队列里面没有元素的了,consumer只能等待了。
           // 按道理来说,这里应该唤醒消费者了来消费了,但是这里并没有这么做。奇怪
            while (count.get() == 0) {
                notEmpty.await();
            }
           // 出队列
            x = dequeue();
            // count--
            c = count.getAndDecrement();
            if (c > 1)
                // 只要count还有,就唤醒继续消费。
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
       // 如果c == capacity,说明,这次consumer消费消费了一个,导致队列中有空闲了。所以,与此同时,就说明,有producer是在
       // 等待的,所以,得唤醒。
        if (c == capacity)
            signalNotFull();
        return x;
    }

dequeue出队

出队的时候是从头结点出的,因为头结点存放的Element是NULL,这里的出队是比较有意思的,一开始我以为是不动头结点,删除头节点的第一个next元素,其实不是,这里删除的是头结点,并且将头结点下移。下面画个图理解理解

出队的时候是从头结点的第一个next节点出队的。

入队的时候是从尾结点入的。

  private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        // assert head.item == null;
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }

  1. 初始状态

    队列里面实际存放了两个元素。现在要出队。
    在这里插入图片描述

  2. 出队

在这里插入图片描述

这应该很清晰了,将头结点的nextNode的Element返回,将头结点的NextNode为头结点,并且将头结点的nextNode的Element变为NULL,将头结点出队。实际返回的值确实头结点nextNode的Element的值。

put线程和take线程何时等待,等待在哪里?何时唤醒,由谁唤醒?

何时阻塞,等待在哪里?

  1. put线程

    对应put线程来说,当队列满的时候(count==capacity)的时候,等待在putLock的Condition里面。

  2. take

    当队列元素的数量为0的时候(count==0)的时候,等待在takeLock的Condition里面。

何时唤醒,由谁唤醒?

  1. put线程

    从上面的代码里面可以看到,有两处唤醒的地方

    • put线程获取锁之后,元素入队之后,判断队列是否没有满(count < capacity),没有满就唤醒putlock的Condition里面等待的put线程
    • put线程在获取元素之后,判断之前队列是不是空的(count0),如果是,就说明在这个时刻,可能有take线程在等待,put操作使得空的队列中有元素了,所以就需要唤醒。为什么说可能呢?因为take等待的条件就是(count0),只不过是时机的问题。
  2. take线程

    • take线程获取锁之后,元素出队之后,判断队列是否还有元素(count > 1 ),如果满足,就唤醒takeLock的Condition中等待的take线程
    • take线程操作完之后,判断之前的队列是否是满的(count == capacity),如果是,就说明在这个时刻,可能有put线程在等待,而take操作会使队列长度-1,队列中有空间的地方了。所以就需要唤醒。为什么说可能呢?因为put等待的条件就是(count==capacity),只不过是时机的问题。

别的方法基本都大同小异,这里就不逐个分析了,掌握核心思想。

关于 LinkedBlockingQueue 的分析就分析到这里了。 如有不正确的地方,欢迎指出。谢谢。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值