前言
LinkedBlockingQueue是一个用链表实现的有界阻塞队列。在并发编程中,其使用的频率非常多。因为其阻塞队列的特性,所以在生产者消费者模型中,经常用来当中间件。生产者就是向队列中添加元素的线程,消费者就是从队列中取元素的线程。阻塞队列生产者用来存放元素,消费者用来获取元素的容器。简介如下:
·阻塞添加
阻塞添加就是指的是当阻塞队列元素已满的时候,队列会阻塞加入元素的线程,直到元素队列不满的时候,才重新唤醒线程执行元素加入操作
·阻塞删除
阻塞删除就是指的是在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不再为空的时候再执行删除操作(一般都会返回被删除的元素)
常用方法简介
1.往队列中存放元素的方法
·add(E e)
·offer(E e)
·put(E e)
2.往队列中取元素的方法
·remove()
·poll()
·take()
用一个表格做一个对比,然后用源码对表格中的内容进行一个解释
方法/处理方法 | 抛出异常 | 返回特殊值 | 一直阻塞 |
插入方法 | add(E e) | offer(E e) | put(E e) |
移除方法 | remove() | poll() | take() |
抛出异常:当队列为满时,如果再往队列中插入元素,则会抛出IllegalStateException("Queue full")异常。当队列为空的时候,从队列中获取元素会抛出NoSuchElementException异常
返回特殊值:当往队列中插入元素的时候,会返回元素是否插入成功;如果是移除方法,则是从中获取一个元素,没有返回null。
一直阻塞:当阻塞队列满的时候,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者相应中断退出。当队列为空的时候,如果消费者线程从队列里take元素,队列会阻塞住消费者线程,直到队列不为空。
源码分析
首先是插入元素的三种方法的源码
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
add方法调用了offer方法,同时也解释了抛出异常的原因,下面则给出offer源码
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;//当前队列的元素
if (count.get() == capacity)//count是否等于最大的容量
return false;
int c = -1;
Node<E> node = new Node<E>(e);//new一个node(当前元素和指向下一个元素的结点)
final ReentrantLock putLock = this.putLock;//上锁
putLock.lock();
try {//核心代码
if (count.get() < capacity) {
enqueue(node);//小于容量,入队
c = count.getAndIncrement();
if (c + 1 < capacity)//没有满
notFull.signal();//唤醒一个线程
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
①首先判断是否达到最大容量,如果达到最大容量返回false,这也就解释了add会抛出异常的原因
②然后上锁操作开始,如果小于容量则入队,入队后如果没有满的话,就唤醒一个线程,最后释放锁
③如果队列为空,则也是唤醒一个线程
最后是put()方法
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;
putLock.lockInterruptibly();//抛出异常的原因
try {
while (count.get() == capacity) {
notFull.await();//放数据的线程等待
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();//没有满的话,就可以加了
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
通过上面代码来看,可以看出来,插入操作如果队列满了的话,则会一直阻塞,直到不满为止。
下面是获取元素的方法
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
首先是remove方法
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
其实这个方法和add方法其实差不多,同样去调用了别的方法,同时解释了其抛出异常的原因
下面看一下poll方法
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();
if (c > 1)//队列非空,继续消费
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)//c==最大容量
signalNotFull();//上面拿出了一个,所以这里唤醒一个加元素的操作
return x;
}
①判断如果为空的话,就返回null
②不为空,上锁,如果队列不空,获得队首元素,如果队列非空,则唤醒线程
③如果c到达了最大容量,唤醒加元素的操作
然后是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)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
取元素如果队列为空的话,会进入一个等待的状态
总结
-add()实际调用的offer,区别是在队列满的时候,add会报异常
-offer()队列如果满了,直接入队失败
-put();在队列满的时候回进入阻塞的状态
-remove();直接调用pool操作,唯一区别是remove会抛出异常,而poll队列为空的时候,返回null
-pool();队列为空的时候直接返回null
-take();队列为空的时候,会进入等待的状态
补充
在上面的操作中,其实也蕴含着CAS操作,以前的像ReentrantLock等类也有很多CAS操作,这里做一下解释。其实CAS全称为CompareAndSwap,即比较并交换。当更新值的时候,先用当前值和旧值进行比较,如果相同则更新,不同则返回一个错误码。其是concurrent存在的基础,CAS实现不是通过代码,而是通过CPU对其的支持,不同的CPU对CAS的实现方式不同,但是最后的效果是一样的。调用CAS的时候,通过调用java的Unsafe类对硬件级别对其进行操作,从而实现比较并交换这一操作,Unsafe类实现了不依赖native方法就可以实现硬件级别就可以实现硬件级别的原子操作。