BlockingQueue

本文介绍如何根据ArrayBlockingQueue源码实现BlockingQueue,重点讲解put()、take()、insert()和extract()方法,避免常见错误。

[转自: https://www.cnblogs.com/techyc/p/3782079.html]

BlockingQueue是多线程里面一个非常重要的数据结构。在面试的时候,也常会被问到怎么实现BlockingQueue。本篇根据Java7里ArrayBlockingQueue的源码,简单介绍一下如何实现一个BlockingQueue。

要实现BlockingQueue,首先得了解最主要的方法

add()和remove()是最原始的方法,也是最不常用的。原因是,当队列满了或者空了的时候,会抛出IllegalStateException("Queue full")/NoSuchElementException(),并不符合我们对阻塞队列的要求;因此,ArrayBlockingQueue里,这两个方法的实现,直接继承自java.util.AbstractQueue:

 1    public boolean add(E e) {
 2         if (offer(e))
 3             return true;
 4         else
 5             throw new IllegalStateException("Queue full");
 6     }
 7 
 8     public E remove() {
 9         E x = poll();
10         if (x != null)
11             return x;
12         else
13             throw new NoSuchElementException();
14     }

有上述源码可知,add()和remove()实现的关键,是来自java.util.Queue接口的offer()和poll()方法。

offer():在队列尾插入一个元素。若成功便返回true,若队列已满则返回false。(This method is generally preferable to method add(java.lang.Object), which can fail to insert an element only by throwing an exception.)

poll():同理,取出并删除队列头的一个元素。若成功便返回true,若队列为空则返回false。

这里使用的是ReentrantLock,在插入或者取出前,都必须获得队列的锁,以保证同步。

 

 1     public boolean offer(E e) {
 2         checkNotNull(e);
 3         final ReentrantLock lock = this.lock;
 4         lock.lock();
 5         try {
 6             if (count == items.length)
 7                 return false;
 8             else {
 9                 insert(e);
10                 return true;
11             }
12         } finally {
13             lock.unlock();
14         }
15     }
16 
17     public E poll() {
18         final ReentrantLock lock = this.lock;
19         lock.lock();
20         try {
21             return (count == 0) ? null : extract();
22         } finally {
23             lock.unlock();
24         }
25     }

由于offer()/poll()是非阻塞方法,一旦队列已满或者已空,均会马上返回结果,也不能达到阻塞队列的目的。因此有了put()/take()这两个阻塞方法:

 

 1     public void put(E e) throws InterruptedException {
 2         checkNotNull(e);
 3         final ReentrantLock lock = this.lock;
 4         lock.lockInterruptibly();
 5         try {
 6             while (count == items.length)
 7                 notFull.await();
 8             insert(e);
 9         } finally {
10             lock.unlock();
11         }
12     }
13 
14     public E take() throws InterruptedException {
15         final ReentrantLock lock = this.lock;
16         lock.lockInterruptibly();
17         try {
18             while (count == 0)
19                 notEmpty.await();
20             return extract();
21         } finally {
22             lock.unlock();
23         }
24     }

put()/take()的实现,比起offer()/poll()复杂了一些,尤其有两个地方值得注意:

1. 取得锁以后,循环判断队列是否已满或者已空,并加上Condition的await()方法将当前正在调用put()的线程挂起,直至notFull.signal()唤起。

2. 这里使用的是lock.lockInterruptibly()而不是lock.lock()。原因在这里。lockInterruptibly()这个方法,优先考虑响应中断,而不是响应普通获得锁或重入获得锁。简单来说就是,由于put()/take()是阻塞方法,一旦有interruption发生,必须马上做出反应,否则可能会一直阻塞。

最后,无论是offer()/poll()还是put()/take(),都要靠insert()/extract()这个私有方法去完成真正的工作:

 

 1     private void insert(E x) {
 2         items[putIndex] = x;
 3         putIndex = inc(putIndex);
 4         ++count;
 5         notEmpty.signal();
 6     }
 7 
 8     final int inc(int i) {
 9         return (++i == items.length) ? 0 : i;
10     }
11 
12     private E extract() {
13         final Object[] items = this.items;
14         E x = this.<E>cast(items[takeIndex]);
15         items[takeIndex] = null;
16         takeIndex = inc(takeIndex);
17         --count;
18         notFull.signal();
19         return x;
20     }
21 
22     final int dec(int i) {
23         return ((i == 0) ? items.length : i) - 1;
24     }

insert()/extract(),是真正将元素放进数组或者将元素从数组取出并删除的方法。由于ArrayBlockingQueue是有界限的队列(Bounded Queue),因此inc()/dec()方法保证元素不超出队列的界限。另外,每当insert()后,要使用notEmpty.signal()唤起因队列空而等待取出的线程;每当extract()后,同理要使用notFull.signal()唤起因队列满而等待插入的线程。

到此,便将ArrayBlockingQueue的主要的方法粗略介绍了一遍。假设面试时,需要我们自己实现BlockingQueue时,可参考以上的做法,重点放在put()/take()和insert()/extract()方法上,也可将其结合在一起:

 

 1 class BoundedBuffer {  
 2   final Lock lock = new ReentrantLock();  
 3   final Condition notFull  = lock.newCondition();   
 4   final Condition notEmpty = lock.newCondition();   
 5   
 6   final Object[] items = new Object[100];  
 7   int putptr, takeptr, count;  
 8   
 9   public void put(Object x) throws InterruptedException {  
10     lock.lock();  
11     try {  
12       while (count == items.length)   
13         notFull.await();  
14       items[putptr] = x;   
15       if (++putptr == items.length) putptr = 0;  
16       ++count;  
17       notEmpty.signal();  
18     } finally {  
19       lock.unlock();  
20     }  
21   }  
22   
23   public Object take() throws InterruptedException {  
24     lock.lock();  
25     try {  
26       while (count == 0)   
27         notEmpty.await();  
28       Object x = items[takeptr];   
29       if (++takeptr == items.length) takeptr = 0;  
30       --count;  
31       notFull.signal();  
32       return x;  
33     } finally {  
34       lock.unlock();  
35     }  
36   }   
37 }

最后,由于此文的启示,列举一些使用队列时的错误做法:

1. 忽略offer()的返回值。offer()作为有返回值的方法,可以在判断的时候十分有作用(例如add()的实现)。因此,千万不要忽略offer()方法的返回值。

2. 在循环里使用isEmpty()和阻塞方法:

1 while(!queue.isEmpty())
2 {
3    T element = queue.take();
4    
5    //Process element.
6 }

take()是阻塞方法,无需做isEmpty()的判断,直接使用即可。而这种情况很有可能导致死锁,因为由于不断循环,锁会一直被isEmpty()取得(因为size()方法会取得锁),而生产者无法获得锁。

3. 频繁使用size()方法去记录。size()方法是要取得锁的,意味着这不是一个廉价的方法。可以使用原子变量代替。

 

本文完

### 关于 Java 中 BlockingQueue 的介绍 #### 定义与概述 `BlockingQueue` 是 Java 并发包 `java.util.concurrent` 提供的一个接口,用于支持在生产者-消费者模式下的线程安全队列操作。该接口扩展了 `Queue` 接口并增加了阻塞功能,在某些情况下当执行插入或删除元素的操作时如果无法立即完成,则会等待一段时间甚至无限期地等待直至条件满足。 #### 方法分类及其行为描述 针对不同的应用场景需求,`BlockingQueue` 设计了几类具有不同特性的方法来处理入队和出队的行为: - **抛出异常**: 当尝试向满的队列中添加新项或将从空队列读取数据时将会触发特定类型的异常。 - 插入: 使用 `add(e)` 尝试加入元素 e;若失败则抛出 `IllegalStateException` 或其他适当异常[^1]。 - 移除: 利用 `remove()` 取消最前面的一项;如果没有可用项目就抛出 NoSuchElementException[]^1]^。 - **返回特殊值 (null/false)** : 这些版本不会阻止调用者的进展而是通过返回 false 表明未成功的动作或是 null 来指示不存在的数据。 - 插入: 应用程序可以使用 `offer(e)` 添加对象到队尾;如果因为容量原因而未能成功则给出布尔型结果表示状态。 - 移除: 对应地有 `poll()` 函数用来获取头部节点的内容;一旦发现为空即刻反馈 null 值作为回应[^2]。 - **无期限阻塞** :此类函数会在必要条件下挂起当前进程直到能够顺利完成预期的任务为止。 - 插入: `put(e)` 实现了将指定实体放置于容器末端的功能;即使空间不足也会持续等待资源释放后再继续工作[^4]。 - 移除: 同样存在 `take()` ,它负责提取首个成员并且只有当确实拥有可访问条目时才会结束休眠状态。 - **限时等待** :允许设定最大超时期限以控制长时间停滞的风险。 - 插入: 用户可通过 `offer(Object o, long timeout, TimeUnit unit)` 设置最长容忍延迟秒数以便决定何时停止重试机制。 - 移除: 类似地提供了带有参数配置选项的 `poll(long timeout, TimeUnit unit)` 方便开发者灵活调整策略。 #### 示例代码展示如何创建及运用一个简单的 BlockingQueue 结构 下面是一个简单例子展示了怎样声明以及初始化一个固定大小的工作任务缓冲区,并演示了一些基本操作: ```java import java.util.concurrent.*; public class SimpleBlockingQueueExample { public static void main(String[] args) throws InterruptedException { // 创建一个容量为5的LinkedBlockingQueue实例 BlockingQueue<String> queue = new LinkedBlockingQueue<>(5); System.out.println("Adding elements..."); try{ // 放置多个字符串进入队列 for(int i=0;i<7;i++){ String message="Task "+i; if(!queue.offer(message)){ System.out.println("Failed to add task due to full capacity."); } } Thread.sleep(2000); //模拟一些时间过去 while (!queue.isEmpty()){ //取出并打印每一个消息 System.out.println(queue.poll()); } }catch(Exception ex){ ex.printStackTrace(); } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值