时间:2021-05-02
在上一章我们讲解了arrayblockingqueue,用数组形式实现的阻塞队列。
数组的长度在创建时就必须确定,如果数组长度小了,那么arrayblockingqueue队列很容易就被阻塞,如果数组长度大了,就容易浪费内存。
而队列这个数据结构天然适合用链表这个形式,而linkedblockingqueue就是使用链表方式实现的阻塞队列。
一. 链表实现
1.1 node内部类
? 1 2 3 4 5 6 7 8 9 10 11 /** * 链表的节点,同时也是通过它来实现一个单向链表 */ static class node<e> { e item; // 指向链表的下一个节点 node<e> next; node(e x) { item = x; } }有一个变量e储存数据,有一个变量next指向下一个节点的引用。它可以实现最简单地单向列表。
1.2 怎样实现链表
? 1 2 3 4 5 6 7 8 9 /** * 它的next指向队列头,这个节点不存储数据 */ transient node<e> head; /** * 队列尾节点 */ private transient node<e> last;要实现链表,必须有两个变量,一个表示链表头head,一个表示链表尾last。head和last都会在linkedblockingqueue对象创建的时候被初始化。
? 1 last = head = new node<e>(null);注意这里用了一个小技巧,链表头head节点并没有存放数据,它指向的下一个节点,才真正存储了链表中第一个数据。而链表尾last的确储存了链表最后一个数据。
1.3 插入和删除节点
? 1 2 3 4 5 6 7 8 9 /** * 向队列尾插入节点 */ private void enqueue(node<e> node) { // assert putlock.isheldbycurrentthread(); // 当前线程肯定获取了putlock锁 // 将原队列尾节点的next引用指向新节点node,然后再将node节点设置成队列尾节点last // 这样就实现了向队列尾插入节点 last = last.next = node; }在链表尾插入节点很简单,将原队列尾last的下一个节点next指向新节点node,再将新节点node赋值给队列尾last节点。这样就实现了插入一个新节点。
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 移除队列头节点,并返回被删除的节点数据 private e dequeue() { // assert takelock.isheldbycurrentthread(); // 当前线程肯定获取了takelock锁 // assert head.item == null; node<e> h = head; // first节点中才存储了队列中第一个元素的数据 node<e> first = h.next; h.next = h; // help gc // 设置新的head值,相当于删除了first节点。因为head节点本身不储存数据 head = first; // 队列头的数据 e x = first.item; // 移除原先的数据 first.item = null; return x; }要注意head并不是链表头,它的next才是指向链表头,所以删除链表头也很简单,就是将head.next赋值给head,然后返回原先head.next节点的数据。
删除的时候,就要注意链表为空的情况。head.next的值使用enqueue方法添加的。当head.next==last的时候,表示已经删除到最后一个元素了,当head.next==null的时候,就不能删除了,因为链表已经为空了。这里没有做判断,是因为在调用dequeue方法的地方已经做过判断了。
二. 同步锁reentrantlock和条件condition
因为阻塞队列在队列为空和队列已满的情况下,都必须阻塞等待,那么就天然需要两个条件。而为了保证多线程并发安全,又需要一个同步锁。这个在arrayblockingqueue中已经说过了。
这里我们来说说linkedblockingqueue不一样的地方。
? 1 2 3 4 5 6 7 8 9 10 11 /** 独占锁,用于处理插入队列操作的并发问题,即put与offer操作 */ private final reentrantlock putlock = new reentrantlock(); /** 队列不满的条件condition,它是由putlock锁生成的 */ private final condition notfull = putlock.newcondition(); /** 独占锁,用于处理删除队列头操作的并发问题,即take与poll操作 */ private final reentrantlock takelock = new reentrantlock(); /** 队列不为空的条件condition, 它使用takelock锁生成的 */ private final condition notempty = takelock.newcondition();2.1 putlock与takelock
我们发现使用了两把锁:
按照上面的说法,可能会出现同时插入元素和删除元素的操作,那么就不会出现问题么?
我们来具体分析一个下,对于队列来说操作分为三种:
因此使用putlock锁来保持last变量的安全,使用takelock锁来保持head变量的安全。
对于都涉及了队列中元素个数count变量,所以使用atomicinteger来保证并发安全问题。
? 1 2 /** 队列中元素的个数,这里使用atomicinteger变量,保证并发安全问题 */ private final atomicinteger count = new atomicinteger();2.2 notfull与notempty
2.3 控制流程
当插入元素时:
当删除元素时:
还要注意一下,condition的signal和await方法必须在获取锁的情况下调用。因此就有了signalnotempty和signalnotfull方法:
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * 唤醒在notempty条件下等待的线程,即移除队列头时,发现队列为空而被迫等待的线程。 * 注意,因为要调用condition的signal方法,必须获取对应的锁,所以这里调用了takelock.lock()方法。 * 当队列中插入元素(即put或offer操作),那么队列肯定不为空,就会调用这个方法。 */ private void signalnotempty() { final reentrantlock takelock = this.takelock; takelock.lock(); try { notempty.signal(); } finally { takelock.unlock(); } } /** * 唤醒在notfull条件下等待的线程,即队列尾添加元素时,发现队列已满而被迫等待的线程。 * 注意,因为要调用condition的signal方法,必须获取对应的锁,所以这里调用了putlock.lock()方法 * 当队列中删除元素(即take或poll操作),那么队列肯定不满,就会调用这个方法。 */ private void signalnotfull() { final reentrantlock putlock = this.putlock; putlock.lock(); try { notfull.signal(); } finally { putlock.unlock(); } }三. 插入元素方法
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public void put(e e) throws interruptedexception { if (e == null) throw new nullpointerexception(); // 记录插入操作前元素的个数 int c = -1; // 创建新节点node node<e> node = new node<e>(e); final reentrantlock putlock = this.putlock; final atomicinteger count = this.count; putlock.lockinterruptibly(); try { //表示队列已满,那么就要调用notfull.await方法,让当前线程等待 while (count.get() == capacity) { notfull.await(); } // 向队列尾插入新元素 enqueue(node); // 将当前队列元素个数加1,并返回加1之前的元素个数。 c = count.getandincrement(); // c + 1 < capacity表示队列未满,就唤醒可能等待插入操作的线程 if (c + 1 < capacity) notfull.signal(); } finally { putlock.unlock(); } // c == 0表示插入之前,队列是空的。队列从空到放入一个元素时, // 才唤醒等待删除的线程 // 防止频繁获取takelock锁,消耗性能 if (c == 0) signalnotempty(); }以put方法为例,大体流程与我们前面介绍一样,这里有一个非常怪异的代码,当插入完元素时,如果发现队列未满,那么调用notfull.signal()唤醒等待插入的线程。
大家就很疑惑了,一般来说,这个方法应该放在删除元素(take系列的方法里),因为当我们删除一个元素,那么队列肯定是未满的,那么调用notfull.signal()方法,唤醒等待插入的线程。
这里这么做主要是因为调用signal方法,必须先获取对应的锁,而在take系列的方法里使用的锁是takelock,那么想调用notfull.signal()方法,必须先获取putlock锁,这样的话会性能就会下降,所以用了另一种方式。
四. 删除队列头元素
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public e take() throws interruptedexception { e x; int c = -1; final atomicinteger count = this.count; final reentrantlock takelock = this.takelock; takelock.lockinterruptibly(); try { //表示队列为空,那么就要调用notempty.await方法,让当前线程等待 while (count.get() == 0) { notempty.await(); } // 删除队列头元素,并返回它 x = dequeue(); // 返回当前队列个数,然后将队列个数减一 c = count.getanddecrement(); // c > 1表示队列不为空,就唤醒可能等待删除操作的线程 if (c > 1) notempty.signal(); } finally { takelock.unlock(); } /** * c == capacity表示删除操作之前,队列是满的。只有从满队列中删除一个元素时, * 才唤醒等待插入的线程 * 防止频繁获取putlock锁,消耗性能 */ if (c == capacity) signalnotfull(); return x; }为什么调用notempty.signal()方法原因,对比一下我们在插入元素方法中的解释。
五. 查看队列头元素
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 查看队列头元素 public e peek() { // 队列为空,返回null if (count.get() == 0) return null; final reentrantlock takelock = this.takelock; takelock.lock(); try { // 获取队列头节点first node<e> first = head.next; // first == null表示队列为空,返回null if (first == null) return null; else // 返回队列头元素 return first.item; } finally { takelock.unlock(); } }查看队列头元素,涉及到head节点,所以必须使用takelock锁。
六. 其他重要方法
6.1 remove(object o)方法
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 从队列中删除指定元素o public boolean remove(object o) { if (o == null) return false; // 因为不是删除列表头元素,所以就涉及到head和last两个变量, // putlock与takelock都要加锁 fullylock(); try { // 遍历整个队列,p表示当前节点,trail表示当前节点的前一个节点 // 因为是单向链表,所以需要记录两个节点 for (node<e> trail = head, p = trail.next; p != null; trail = p, p = p.next) { // 如果找到了指定元素,那么删除节点p if (o.equals(p.item)) { unlink(p, trail); return true; } } return false; } finally { fullyunlock(); } }从列表中删除指定元素,因为删除的元素不一定在列表头,所以可能会head和last两个变量,所以必须同时使用putlock与takelock两把锁。因为是单向链表,需要一个辅助变量trail来记录前一个节点,这样才能删除当前节点p。
6.2 unlink(node<e> p, node<e> trail) 方法
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 删除当前节点p,trail代表p的前一个节点 void unlink(node<e> p, node<e> trail) { // 将当前节点的数据设置为null p.item = null; // 这样就在链表中删除了节点p trail.next = p.next; // 如果节点p是队列尾last,而它被删除了,那么就要将trail设置为last if (last == p) last = trail; // 将元素个数减一,如果原队列是满的,那么就调用notfull.signal()方法 // 其实这个不用判断直接调用的,因为这里肯定获取了putlock锁 if (count.getanddecrement() == capacity) notfull.signal(); }要在链表中删除一个节点p,只需要将p的前一个节点trail的next指向节点p的下一个节点next。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://www.jianshu.com/p/fb79f074be28
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
本文通过实例给大家详细分析了JS中事件循环机制的原理和用法,以下是全部内容:varstart=newDate()setTimeout(function(){va
详解Java中Comparable和Comparator接口的区别本文要来详细分析一下Java中Comparable和Comparator接口的区别,两者都有比
本文详细分析了C#类的访问修饰符用法,分享给大家供大家参考。具体用法分析如下:默认情况下,类声明为内部的,即只有当前工程中的代码才能访问它。可以用interna
本文详细分析了smarty缓存的用法。分享给大家供大家参考。具体分析如下:一开始以为smarty只是用来做一些掩饰php代码功能,但是后来才知道还有模板缓存这个
以下内容通过1、实现目标注入程序,2、实现主程序,3、实现注入函数,4、thumb指令集实现等4个方面详细分析了android中inlinehook的用法,以下