自己实现
在自己实现之前先搞清楚阻塞队列的几个特点:
- 基本队列特性:先进先出。
- 写入队列空间不可用时会阻塞。
- 获取队列数据时当队列为空时将阻塞。
实现队列的方式多种,总的来说就是数组和链表;其实我们只需要搞清楚其中一个即可,不同的特性主要表现为数组和链表的区别。
这里的 ArrayBlockingQueue
看名字很明显是由数组实现。
我们先根据它这三个特性尝试自己实现试试。
初始化队列
我这里自定义了一个类:ArrayQueue
,它的构造函数如下:
public ArrayQueue(int size) { items = new Object[size]; }
很明显这里的 items
就是存放数据的数组;在初始化时需要根据大小创建数组。
写入队列
写入队列比较简单,只需要依次把数据存放到这个数组中即可,如下图:
但还是有几个需要注意的点:
- 队列满的时候,写入的线程需要被阻塞。
- 写入过队列的数量大于队列大小时需要从第一个下标开始写。
先看第一个队列满的时候,写入的线程需要被阻塞
,先来考虑下如何才能使一个线程被阻塞,看起来的表象线程卡住啥事也做不了。
有几种方案可以实现这个效果:
Thread.sleep(timeout)
线程休眠。
object.wait()
让线程进入waiting
状态。
当然还有一些
join、LockSupport.part
等不在本次的讨论范围。
阻塞队列还有一个非常重要的特性是:当队列空间可用时(取出队列),写入线程需要被唤醒让数据可以写入进去。
所以很明显Thread.sleep(timeout)
不合适,它在到达超时时间之后便会继续运行;达不到空间可用时才唤醒继续运行这个特点。
其实这样的一个特点很容易让我们想到 Java 的等待通知机制来实现线程间通信;更多线程见通信的方案可以参考这里:深入理解线程通信
所以我这里的做法是,一旦队列满时就将写入线程调用 object.wait()
进入 waiting
状态,直到空间可用时再进行唤醒。
/** * 队列满时的阻塞锁 */ private Object full = new Object(); /** * 队列空时的阻塞锁 */ private Object empty = new Object();
所以这里声明了两个对象用于队列满、空情况下的互相通知作用。
在写入数据成功后需要使用 empty.notify()
,这样的目的是当获取队列为空时,一旦写入数据成功就可以把消费队列的线程唤醒。
这里的 wait 和 notify 操作都需要对各自的对象使用
synchronized
方法块,这是因为 wait 和 notify 都需要获取到各自的锁。
消费队列
上文也提到了:当队列为空时,获取队列的线程需要被阻塞,直到队列中有数据时才被唤醒。
代码和写入的非常类似,也很好理解;只是这里的等待、唤醒恰好是相反的,通过下面这张图可以很好理解:
总的来说就是:
- 写入队列满时会阻塞直到获取线程消费了队列数据后唤醒写入线程。
- 消费队列空时会阻塞直到写入线程写入了队列数据后唤醒消费线程。