在新增的Concurrent包中,BlockingQueue很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。
1、 认识BlockingQueue
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
常用的队列主要有以下两种:(当然通过不同的实现方式,还可以延伸出很多不同类型的队列,DelayQueue就是其中的一种)
先进先出(FIFO):先插入的队列的元素也最先出队列,类似于排队的功能。从某种程度上来说这种队列也体现了一种公平性。
后进先出(LIFO):后插入队列的元素最先出队列,这种队列优先处理最近发生的事件。
下面两幅图演示了BlockingQueue的两个常见阻塞场景:
如上图所示:当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
如上图所示:当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
这也是我们在多线程环境下,为什么需要BlockingQueue的原因。作为BlockingQueue的使用者,我们再也不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。
2、BlockingQueue具体实现
首先BlockingQueue是一个接口,它的实现类是这些
里面有基于数组实现的ArrayBlockingQueue,适合有界队列和基于链表实现的LinkedBlockingQueue,适合无界队列,因为长度可以动态增加
因为BlockingQueue会让服务线程(不断从队列中取任务,然后进行处理的线程)在队列为空时进行等待,当新的任务加入队列,就会唤醒线程去处理,想了解怎么实现可以看ArrayBlockingQueue源码
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
主要看put()和take(),put()是将元素压入队列中,若队列满了,就等待直到有空闲的位置,
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)//当队列满的时候
notFull.await();//等待
enqueue(e);
} finally {
lock.unlock();
}
}
当有元素退出队列出现空位时,就通知等待入队的线程
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
而take()是从队列取元素,若队列为空就等待,直到有新的元素加入
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)//当队列为空
notEmpty.await();//等待
return dequeue();
} finally {
lock.unlock();
}
}
当然了,当队列有元素添加进来,就给线程一个通知
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();//通知
}
3、BlockingQueue实现
下面程序展示如何使用阻塞队列来控制线程集,程序在一个目录以及目录下的所有文件,打印出包含指定关键字的文件列表
生产者线程:枚举所有子目录下所有文件放进队列中,最后放进模拟对象,作为结束信号
public class FileEnumeratonTask implements Runnable{
private BlockingQueue<File> queue;
private File startingDirectory;
//模拟对象,当搜索线程取到该对象,就发出完成信号,将其返回并终止
public static File DUMMY=new File("");
public FileEnumeratonTask(BlockingQueue<File> queue, File startingDirectory) {
this.queue = queue;
this.startingDirectory = startingDirectory;
}
@Override
public void run() {
try {
enumerate(startingDirectory);
queue.put(DUMMY);//最后放入模拟对象,标志工作结束
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//把所有文件放入阻塞队列
public void enumerate(File directory) throws InterruptedException{
File[] files=directory.listFiles();
for(File file:files){
if(file.isDirectory())//如果是目录就递归,是文件就放入阻塞队列
enumerate(file);
else
queue.put(file);
}
}
}
消费者线程:即搜索线程,每个搜索线程在队列取一个文件,打开它,打印包含关键字的所有行
public class SearchTask implements Runnable {
private BlockingQueue<File> queue;
private String keyword;
public SearchTask(BlockingQueue<File> queue, String keyword) {
super();
this.queue = queue;
this.keyword = keyword;
}
@Override
public void run() {
try {
boolean isDone=false;
if(!isDone){
File file;
file = queue.take();
if(file==FileEnumeratonTask.DUMMY){
queue.add(file);
isDone=true;
}else
search(file);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void search(File file) {
try {
Scanner in=new Scanner(new FileInputStream(file));
int lineNumber=0;
while(in.hasNextLine()){
lineNumber++;
String line=in.nextLine();
if(line.contains(keyword)){
System.out.printf("%s:%d:%s%n",file.getPath(),lineNumber,line);
}
}
in.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public class BlockingQueueTest {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
System.out.println("enter base directory:");
String directory=in.nextLine();
System.out.println("enter keyword:");
String keyword=in.nextLine();
final int FILE_QUEUE_COUNT=10;
final int SEARCH_COUNT=100;
BlockingQueue<File> queue=new ArrayBlockingQueue(FILE_QUEUE_COUNT);
FileEnumeratonTask fileEnumeratonTask=new FileEnumeratonTask(queue, new File(directory));
new Thread(fileEnumeratonTask).start();
for(int i=0;i<SEARCH_COUNT;i++){
new Thread(new SearchTask(queue,keyword)).start();
}
}
}