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等待的线程)。
提示:
-
count.getAndIncrement()
count.getAndIncrement() 在语义上相当于 count++。如果count=2, c = count++, count和c等于多少?
count等于3,c等于2。
理解这个对下面的唤醒take线程有帮助
-
为什么在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;
}
- 队列初始化
-
入队
按照上面入队代码的逻辑就是这个样子,上面的写法是很简单,并且很简洁,很厉害
但要注意,这个时候头结点里面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;
}
-
初始状态
队列里面实际存放了两个元素。现在要出队。
-
出队
这应该很清晰了,将头结点的nextNode的Element返回,将头结点的NextNode为头结点,并且将头结点的nextNode的Element变为NULL,将头结点出队。实际返回的值确实头结点nextNode的Element的值。
put线程和take线程何时等待,等待在哪里?何时唤醒,由谁唤醒?
何时阻塞,等待在哪里?
put线程
对应put线程来说,当队列满的时候(count==capacity)的时候,等待在putLock的Condition里面。
take
当队列元素的数量为0的时候(count==0)的时候,等待在takeLock的Condition里面。
何时唤醒,由谁唤醒?
put线程
从上面的代码里面可以看到,有两处唤醒的地方
- put线程获取锁之后,元素入队之后,判断队列是否没有满(count < capacity),没有满就唤醒putlock的Condition里面等待的put线程
- put线程在获取元素之后,判断
之前
队列是不是空的(count0),如果是,就说明在这个时刻,可能有take线程在等待,put操作使得空的队列中有元素了,所以就需要唤醒。为什么说可能呢?因为take等待的条件就是(count0),只不过是时机的问题。take线程
- take线程获取锁之后,元素出队之后,判断队列是否还有元素(count > 1 ),如果满足,就唤醒takeLock的Condition中等待的take线程
- take线程操作完之后,判断
之前
的队列是否是满的(count == capacity),如果是,就说明在这个时刻,可能有put线程在等待,而take操作会使队列长度-1,队列中有空间的地方了。所以就需要唤醒。为什么说可能呢?因为put等待的条件就是(count==capacity),只不过是时机的问题。
别的方法基本都大同小异,这里就不逐个分析了,掌握核心思想。
关于 LinkedBlockingQueue 的分析就分析到这里了。 如有不正确的地方,欢迎指出。谢谢。