先给出它的UML图
从上图中,可以看出,实现了BlockingQueue接口,其次扩展了AbstractQueue接口,最后还实现Serializable接口,这是个标志性接口,使得队列具有序列化的功能,其实主要就是可以网络传输(网络IO),因为跨越JVM了,传输需要转为字节流。
LinkedBlockingQueue
从名字可以知道,这个队列是以链表的方式存储队列的元素,链表么其实就是火车呀,一节节车厢就是一个个的节点呀。那首先看一下链表节点:
static class Node<E> {
/**
* 节点元素内容
*/
E item;
/**
* 指向后继节点的引用(也可以说是指针)
*/
Node<E> next;
/**
* 构造函数
*/
Node(E x) { item = x; }
}
这是一个静态的内部类,为什么内部类么是因为主要就是它自己用呀,当然这里访问权限默认为protected,它同包下的其他类硬要用也是可以的吧
下面再来看下,成员变量:
/** 队列长度,默认为 Integer.MAX_VALUE */
private final int capacity;
/**
* 队列内的元素
* 这里使用了JUC包下的原子整型保证了count修改的线程安全性,具体是通过CAS实现。
*/
private final AtomicInteger count = new AtomicInteger();
/**
* 表头指针
* Invariant: head.item == null
* 可以看到transient修饰,这个字段不会被序列化
*/
transient Node<E> head;
/**
* 表尾指针
* Invariant: last.next == null
* 可以看到transient修饰,这个字段不会被序列化
*/
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();
小结:
从成员变量看,阻塞队列就是从队列中去元素和放元素需要做同步处理的队列。其实我们学的生产者-消费者问题就能用这个解决,可以看成N个消费者(取元素的线程),M个生产者(放元素的线程)。
下面再看下成员方法:
- 首先当然是看下构造方法:
/**
* 默认构造(无参构造函数)
* 设置队列容量为Integer.MAX_VALUE,可以视为一个无界队列。
* 调用了另外一个构造函数
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
* 用户指定容量的构造函数
* 创建的容量必须是一个正整数,否则报参数异常
* 可以看到,这里创建了一个内容为空的节点,这个设计还是比较好的,这样一来
就不用每次插入都去判断是不是空,直接插入队列尾部,然后移动队尾指针就好。
过会看它的enqueue方法就能看出来。同样删除元素也很方便,见dequeue方法。
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
//创建一个内容为空的头结点,头,尾都指向这个空节点
last = head = new Node<E>(null);
}
/**
* 这个方法是用一个已知的集合来创建队列
* 首先创建一个无界队列
*/
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));
//调整计数
++n;
}
//设置队列的元素数量
count.set(n);
} finally {
//释放锁
putLock.unlock();
}
}
- 队列元素插入删除的实际操作
插入
/**
* 由于有头节点的存在,不需要判断队列是否为空(因为空的时候,队列里有一个内容为空的头结点,队尾
指针指向这个内容为空的节点)
* 新增一个元素,只需要直接把它插到队尾,然后调整队尾指针即可
*/
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
删除
/**
* head指向的是内容为空的头节点,每次实际要删除的是头节点的next,然后这个被删节点作为
新的头结点,如下图:
head last
Node(null)-> Node(A) -> Node(B) -> Node(C)
null 是指Node的item为null,不是说节点为null
取元素就是把A返回,把head移动到Node(A)的位置,然后Node(A)的item设为null
old head head last
Node(null) Node(null) -> Node(B) -> Node(C)
*/
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;
}
- 阻塞队列的同步队列操作
/**
这个方法在往队列里放置元素的时候,如果队列满了就会阻塞等待至队列为不满
*/
public void put(E e) throws InterruptedException {
//添加空元素抛空指针异常
if (e == null) throw new NullPointerException();
final int c;
//要添加的节点
final Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
//抢锁来锁定添加元素
putLock.lockInterruptibly();
try {
/*
* 这里有一段解释为什么count修改不用锁保护
* 首先,count是原子类型的,它的修改是线程安全的,即多个线程去操作不论加还是减
最终结果肯定是对的。所以放和拿是可以同时执行的。
* 其次,在这里其他能够放置而修改count的线程要么就是拿不到锁进不来,要么就是在等待notFull
从而不会修改count
* 还有就是,这里只要判断能不能放,保证c + 1 < capacity,c其实具体是几不重要,也就是取的线程不
会干扰放置这个动作
*/
//只要队列满了,就死等,因为这个时候放置的入口已经卡死,只能是减
while (count.get() == capacity) {
notFull.await();
}
//加入队列
enqueue(node);
//获取队列元素数量,之后可能会变,因为这个时候还是可以取出元素的
//这里是先获取当前情况,再自增1
c = count.getAndIncrement();
//判断还能放元素
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
/**
* 这次put之前,队列为空,说明可能有后续的取元素的线程在等待
* 这里唤醒操作还上了锁,大概是为了只唤醒一个等待取元素的线程,防止多次唤醒同一线程
*/
if (c == 0)
signalNotEmpty();
}
/**
* 这个方法与put相似,只是多了一个等待延迟
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
final int c;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
//检查等待时间有没有过,每次被唤醒都要检查,超时返回false表示插入失败
if (nanos <= 0L)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(new Node<E>(e));
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
/**
* 从队列头部取元素,队列为空的时候,会阻塞等待至有元素
*/
public E take() throws InterruptedException {
final E x;
final int c;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
//上锁
takeLock.lockInterruptibly();
try {
/**死等队列有元素,和put一样,只会有一个取的线程能成判断往下走
,所以这里count只能是增加*/
while (count.get() == 0) {
notEmpty.await();
}
x = dequeue();
//当前时刻的元素数量并对其减1
c = count.getAndDecrement();
//队列还有元素,就唤醒取的线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
//如果取的时候队列是满的,说明可能有放的线程在等,将其唤醒
//这里的唤醒操作还上了锁,大概是为了同一时刻只唤醒一个放置的线程
if (c == capacity)
signalNotFull();
return x;
}
/**
* 比take多了一个阻塞等待的延迟
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
final E x;
final int c;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0L)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
/**
* 从队列中删除某个元素,这个操作需要锁定放和取
*/
public boolean remove(Object o) {
if (o == null) return false;
//放和取都锁定
fullyLock();
try {
//用两个指针遍历删除元素,每次判断后一个指针是否为空或者该删的元素
for (Node<E> pred = head, p = pred.next;
p != null;
pred = p, p = p.next) {
if (o.equals(p.item)) {
unlink(p, pred);
return true;
}
}
return false;
} finally {
fullyUnlock();
}
}
/**
* 判断是否包含某个元素,也是全球锁定,不让放,不然取
*/
public boolean contains(Object o) {
if (o == null) return false;
//全球锁定
fullyLock();
try {
for (Node<E> p = head.next; p != null; p = p.next)
if (o.equals(p.item))
return true;
return false;
} finally {
fullyUnlock();
}
}
- 非阻塞操作
/**
* 队列满了,直接返回false,其他操作和put相似
*/
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
//先快速检查一下,是不是满,防止频繁的上锁
if (count.get() == capacity)
return false;
final int c;
final Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() == capacity)
return false;
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
/**
* 和take相似,只是不再阻塞,没有元素时候直接返回null
*/
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
final E x;
final int c;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() == 0)
return null;
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
/**
* 只取元素,不删除节点
* 不阻塞,没有元素时候直接返回null
*/
public E peek() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
return (count.get() > 0) ? head.next.item : null;
} finally {
takeLock.unlock();
}
}
通知可以取
/**
* 唤醒取元素的线程,告诉他们队列里有东西了,你们可以来取了。
* 然而唤醒谁,这个就看使用的锁是不是公平锁了。默认是非公平的
也就是对一批等待的线程吼一嗓子,你们取吧,然后谁拿到锁谁就可以取元素。
* 可以看到这里也加了取锁,应该是为了一次唤醒一个吧
*/
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
通知可以放
/**
* 这个和signalNotEmpty相似,是通知放置元素的线程,队列有空位置了,你们可以放元素了。
*/
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
小结:
阻塞队列通过加锁和条件变量实现线程同步
需要注意的是,条件变量必须和锁同时使用,其次判断队列状态要用while循环,因为await方法是会释放锁的,不用死等的话,会出现多个线程在await出等待,然后唤醒就不再去加锁,直接往下走。。。这样就出问题了。