并发编程之 LinkedBolckingQueue 源码剖析
前言
JDK 1.5 之后,Doug Lea 大神为我们写了很多的工具,整个 concurrent 包基本都是他写的。也为我们程序员写好了很多工具,包括我们之前说的线程池,重入锁,线程协作工具,ConcurrentHashMap 等等,今天我们要讲的是和 ConcurrentHashMap 类似的数据结构,LinkedBolckingQueue,阻塞队列。在生产者消费者模型中,该类可以帮助我们快速的实现业务功能。
- 如何使用?
- 源码分析
1. 如何使用?
我们在生产者消费者模型,生产者向一个数据共享通道存放数据,消费者从相同的数据共享通道获取数据,将生产和消费完全隔离,不仅是生产者消费者,现在流行的消息队列,比如各种MQ,kafka,和这个都差不多。废话不多说,直接来个demo ,看看怎么使用:
public static void main(String[] args) {
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(1024);
for (int i = 0; i < 5; i++) {
final int num = i;
new Thread(() -> {
try {
for (int j = 0; ; j++) {
linkedBlockingQueue.put(num + "号线程的" + j + "号商品");
Thread.sleep(5000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
for (; ; ) {
System.out.println("消费了" + linkedBlockingQueue.take());
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
运行结果:
消费了0号线程的0号商品
消费了3号线程的0号商品
消费了2号线程的0号商品
消费了1号线程的0号商品
消费了4号线程的0号商品
消费了2号线程的1号商品
消费了1号线程的1号商品
消费了0号线程的1号商品
消费了3号线程的1号商品
消费了4号线程的1号商品
消费了1号线程的2号商品
消费了0号线程的2号商品
消费了2号线程的2号商品
消费了3号线程的2号商品
消费了4号线程的2号商品
·········
从上面的代码中,我们使用了5条线程分别向队列中插入数据,也就是一个字符串,然后让5个线程从队列中取出数据并打印,可以看到,生产者插入的数据从消费者线程中被打印,没有漏掉一个。
注意,这里的 put 方法和 take 方法都是阻塞的,不然就不是阻塞队列了,什么意思呢?如果队列满了,put 方法就会等待,直到队列有空为止,因此该方法使用时需要注意,如果业务即时性很高,那么最好使用带有超时选项的 offer (V,long,TimeUnit),方法,同样, take 方法也是如此,当队列中没有的时候,就会阻塞,直到队列中有数据为止。同样可以使用 poll(long, TimeUnit)方法超时退出。
当然不止这几个方法,楼主将常用的方法总结一下:
插入方法:
// 如果满了,立即返回false
boolean b = linkedBlockingQueue.offer("");
// 如果满了,则等待到给定的时间,如果还满,则返回false
boolean b2 = linkedBlockingQueue.offer("", 1000, TimeUnit.MILLISECONDS);
// 阻塞直到插入为止
linkedBlockingQueue.put("");
取出方法:
// 如果队列为空,直接返回null
Object o3 = linkedBlockingQueue.poll();
// 如果队列为空,一直阻塞到给定的时间
Object o1 = linkedBlockingQueue.poll(1000, TimeUnit.MILLISECONDS);
// 阻塞,直到取出数据
Object o = linkedBlockingQueue.take();
// 获取但不移除此队列的头;如果此队列为空,则返回 null。
Object peek = linkedBlockingQueue.peek();
那么这些方法内部是如何实现的呢?
2. 源码分析
阻塞队列,重点看 put 阻塞方法和 take 阻塞方法。
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 {
/*
* Note that count is used in wait guard even though it is
* not protected by lock. This works because count can
* only decrease at this point (all other puts are shut
* out by lock), and we (or some other waiting put) are
* signalled if it ever changes from capacity. Similarly
* for all other uses of count in other wait guards.
*/
while (count.get() == capacity) {
notFull.await();
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
该方法步骤如下:
- 根据给定的值创建一个 Node 对象,该对象有2个属性,一个是 item,一个是 Node 类型的 next,是链表结构的节点。
- 获取 put 的锁,注意,这里,put 锁和 take 锁是分开的。也就是说,当你插入的时候和取出的时候用的不是一把锁,可以高效并发,但是如果两个线程同时插入就会阻塞。
- 获取链表的长度。
- 使用中断锁,如果调用了线程的中断方法,那么,处于阻塞中的线程就会抛出异常。
- 判断如果当前链表长度达到了设置的长度,默认是 int 最大型,就调用 put 锁的伙伴 Condition 对象 notFull 让当前线程挂起等待。 直到 take 方法中会调用 notFull 对象的 signal 方法唤醒。
- 调用 enqueue 方法,将刚刚创建的 Node 节点连接到链表上。
- 将链表长度变量 count 加一。 判断如果加一后,链表长度还小于链表规定的容量,那么就唤醒其他等待在 notFull 对象上的线程,告诉他们可以取数据了。
- 放开锁,让其他线程争夺锁(非公平锁)。
- 如果c是0,表示队列已经有一个数据了,通知唤醒挂在 notEmpty 的线程,告诉他们可以取数据了。
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;
}
步骤如下:
- 获取链长度,获取 take 锁。
- 调用可中断的 lock 方法。开始锁住。
- 如果队列是空,则挂起线程。开始等待。
- 如果不为空,则调用 dequeue 方法,拿到头节点的数据,并将头节点更新。
- 将队列长度减一。判断如果队列长度大于1,通知等待在 notEmpty 上的线程,可以拿数据了。
- 解锁。
- 如果变量 c 和 容量相同,而刚刚又消费了一个节点,说明队列不满了,则通知生产者可以添加数据了。
- 返回数据。
boolean offer(E e, long timeout, TimeUnit unit) 源码:
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0)
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;
}
该方法会阻塞给定的时间,如果时间到了,则返回false。
和 put 方法很相似,步骤如下:
- 将时间转成纳秒。
- 获取 put 锁。
- 调用可中断锁方法。
- 如果容量满了,并且设置的等待时间小于0,返回 false,表示插入失败,反之,调用 notFull 方法等待给定的时间,并返回一个负数,当第二次循环的时候,继续判断,如果还是满的并且小于0,返回false。
- 如果容量没有满,或者等待过程被唤醒,则调用 enqueue 插入数据。
- 获取当前链表长度。
- 判断链表长度+1是否小于设置的容量。如果小于,则链表没有满,通知生产者可以添加数据了。
- 释放锁。 如果 c 等于 0,表示之前没有数据,但是现在已经加入一个数据了,可以通知其他的消费者来消费了。
- 返回 true。
E poll(long timeout, TimeUnit unit) 源码分析
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0)
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;
}
该方法会阻塞给定的时间,如果取不到数据,返回null。
步骤其实和上面的差不多,楼主偷个懒,就不解释了。
总结
从源码分析中,我们可以看到,整个阻塞队列就是由重入锁和Condition 组合实现的,和我们之前用 synchronized 加上 wait 和 notify 实现很相似,只是楼主的那个例子没有使用队列,因此无法将锁分开,也就是我们之前说的锁分离的技术。那么,整体的性能当然不能和 Doug Lea 大神的比了。
好了。今天的并发源码分析,就到这里。
good luck!!!!
并发编程之 LinkedBolckingQueue 源码剖析的更多相关文章
- 并发编程之 ConcurrentLinkedQueue 源码剖析
前言 今天我们继续分析 java 并发包的源码,今天的主角是谁呢?ConcurrentLinkedQueue,上次我们分析了并发下 ArrayList 的替代 CopyOnWriteArrayList ...
- 并发编程之 CopyOnWriteArrayList 源码剖析
前言 ArrayList 是一个不安全的容器,在多线程调用 add 方法的时候会出现 ArrayIndexOutOfBoundsException 异常,而 Vector 虽然安全,但由于其 add ...
- 并发编程之 ThreadLocal 源码剖析
前言 首先看看 JDK 文档的描述: 该类提供了线程局部 (thread-local) 变量.这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局 ...
- 并发编程之 AQS 源码剖析
前言 JDK 1.5 的 java.util.concurrent.locks 包中都是锁,其中有一个抽象类 AbstractQueuedSynchronizer (抽象队列同步器),也就是 AQS, ...
- 并发编程之 Exchanger 源码分析
前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...
- 并发编程之 Condition 源码分析
前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...
- 并发编程之 Semaphore 源码分析
前言 并发 JUC 包提供了很多工具类,比如之前说的 CountDownLatch,CyclicBarrier ,今天说说这个 Semaphore--信号量,关于他的使用请查看往期文章并发编程之 线程 ...
- 并发编程之 CyclicBarrier 源码分析
前言 在之前的介绍 CountDownLatch 的文章中,CountDown 可以实现多个线程协调,在所有指定线程完成后,主线程才执行任务. 但是,CountDownLatch 有个缺陷,这点 JD ...
- 多线程进阶——JUC并发编程之CountDownLatch源码一探究竟
1.学习切入点 JDK的并发包中提供了几个非常有用的并发工具类. CountDownLatch. CyclicBarrier和 Semaphore工具类提供了一种并发流程控制的手段.本文将介绍Coun ...
随机推荐
- 【计算机网络】网络层学习笔记:总结IP,NAT和DHCP
前言:这篇文章是学习网络层协议时候总结的笔记,前面的主要部分介绍的都是IP协议, 后半部分介绍NAT协议和DHCP协议 参考书籍 <计算机网络-自顶向下> 作者 James F ...
- lost+found目录有啥用?
Linux系统中根目录下或者新挂载的磁盘目录下有一个叫lost+found,它的作用是什么? 如果你运行fsck命令(文件系统检查和修复命令),它也许会找到一些数据碎片,这些文件碎片在硬盘中并没有引用 ...
- [BZOJ2738]矩阵乘法(整体二分+二维树状数组)
整体二分+二维树状数组. 好题啊!写了一个来小时. 一看这道题,主席树不会搞,只能用离线的做法了. 整体二分真是个好东西,啥都可以搞,尤其是区间第 \(k\) 大这种东西. 我们二分答案,然后用二维树 ...
- .NET Core 从1.1升级到2.0记录(Cookie中间件踩坑)
.NET Core 2.0 新时代 万众瞩目的.NET Core 2.0终于发布了,原定于9.19的dotnetconf大会的发布时间大大提前了1个月,.NET Core 2.0/.NET Stand ...
- 在express3里用ejs模版引擎时,如何使其支持'.html'后缀
①express 默认jade模板,改为ejs模板,需执行以下命令: express -e --ejs ②在app.js中,将 app.set('view engine', 'jade'); 替换为 ...
- requests请求例子
实例一: class GetSalerInfo(View): def post(self, request): userid = request.POST/GET.get('userid',None) ...
- 《deep sort》复现过程
目录 1. 准备代码与数据 deep_sort开源代码 克隆到本地服务器 git clone https://github.com/nwojke/deep_sort.git 下载MOT16数据集(MO ...
- WebDriver高级应用实例(7)
7.1在测试中断言失败的步骤进行屏幕截图 目的:在测试过程中,在断言语句执行失败时,对当前的浏览器进行截屏,并在磁盘上新建一个yyyy-mm-dd格式的目录,并在断言失败时新建一个已hh-mm-ss格 ...
- iOS UIFileSharingEnabled
一.让iOS App通过iTunes进行文件交换Documents 让iOS App通过iTunes进行文件交换 有一些App需要通过使用iTunes让用户上传和下载文档.要让iOS程序支持iTune ...
- Delphi:程序自己删除自己,适用于任何windows版本(含源码)
Delphi:程序自己删除自己,适用于任何windows版本(含源码) function Suicide: Boolean; var sei: TSHELLEXECUTEINFO; szMod ...