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

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

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

    public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
} public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}

有上述源码可知,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,在插入或者取出前,都必须获得队列的锁,以保证同步。

     public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
insert(e);
return true;
}
} finally {
lock.unlock();
}
} public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : extract();
} finally {
lock.unlock();
}
}

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

     public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
} public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}

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()这个私有方法去完成真正的工作:

     private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
} final int inc(int i) {
return (++i == items.length) ? 0 : i;
} private E extract() {
final Object[] items = this.items;
E x = this.<E>cast(items[takeIndex]);
items[takeIndex] = null;
takeIndex = inc(takeIndex);
--count;
notFull.signal();
return x;
} final int dec(int i) {
return ((i == 0) ? items.length : i) - 1;
}

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

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

 class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition(); final Object[] items = new Object[100];
int putptr, takeptr, count; public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
} public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}

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

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

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

 while(!queue.isEmpty())
{
T element = queue.take(); //Process element.
}

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

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

本文完

最新文章

  1. haproxy 实现多域名证书https
  2. 误设PATH导致命令失效的处理
  3. Python3基础 print 查看一个列表中存储的所有内容
  4. 20145305 《Java程序设计》第6周学习总结
  5. 浅谈对git的认识
  6. Eclipse部署多个Web项目内存溢出,java.lang.OutOfMemoryError: PermGen space
  7. 惠普 Compaq 6520s 无线开关打不开
  8. hdu 4856 Tunnels
  9. HDU 1010Tempter of the Bone(奇偶剪枝回溯dfs)
  10. 迷你MVVM框架 avalonjs 0.82发布
  11. C#判断文字是否为汉字
  12. 基于react+react-router+redux+socket.io+koa开发一个聊天室
  13. 分享:使用 TypeScript 编写的游戏代码
  14. Android中Bitmap对象和字节流之间的相互转换
  15. Swagger2限定接口范围
  16. 关于npm --save还是-save的横岗数量的细节的记录
  17. PAT A1104 Sum of Number Segments (20 分)——数学规律,long long
  18. docker部署安装
  19. nagle算法和TCP_NODELAY
  20. 一些Go操作Kafka的问题

热门文章

  1. 使用MyEclipse中servlet对SQL Server 2008的CRUD
  2. 字符编码笔记:ASCII,Unicode和UTF-8
  3. 谷歌浏览器下载地址 chrome最新版本 百度云地址
  4. SharePoint 2013 对二进制大型对象(BLOB)进行爬网
  5. [Infopath]使用jquery给infopath表单的的field赋值。 how to set value to Infopath field by Jquery
  6. HashMap和SparseArray的性能比较。
  7. iOS 二维码扫描
  8. Nagios学习实践系列——配置研究[监控当前服务器]
  9. SQL SERVER中隐式转换的一些细节浅析
  10. 【推荐】CentOS安装Tomcat-7.0.57+启动配置+安全配置+性能配置