铁文整理
14.6 阻塞队列
现在,读者已经看到了形成Java并发程序设计基础的底层构建块。然而,对于实际编程来说,应该尽可能远离底层结构。使用由并发处理的专业人士实现的较高层次的结构要方便得多、要安全得多。
对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程则取出它们。使用队列,可以安全地从一个线程向另一个线程传递数据。例如,考虑银行转账程序,转账线程将转账指令对象插入一个队列中,而不是直接访问银行对象。另一个线程从队列中取出指令执行转账。只有该线程可以访问该银行对象的内部。因此不需要同步。(当然,线程安全的队列类的实现者不能不考虑锁和条件。但是,那是他们的问题而不是你的问题。)
当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列(blocking queue)导致线程阻塞。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。工作者线程可以周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步加以修改。队列会自动地平衡负载。如果第一个线程集运行得比第二个慢,第二个线程集在等待结果时会阻塞。如果第一个线程集运行得快,它将等待第二个队列集赶上来。表14-1给出了阻塞队列的方法。
表14-1 阻塞队列方法
方法 | 正常动作 | 特殊情况下的动作。 |
add | 添加一个元素 | 如果队列满,则抛出IllegalStateException异常 |
element | 返回队列的头元素 | 如果队列空,抛出NoSuchElementException异常 |
offer | 添加一个元素并返回true | 如果队列满,返回false |
peek | 返回队列的头元素 | 如果队列空,则返回null |
poll | 移出并返回队列的头元素 | 如果队列空,则返回null |
put | 添加一个元素如 | 果队列满,则阻塞 |
remove | 移出并返回头元素 | 如果队列空,则抛出NoSuchElementException异常 |
take | 移出并返回头元素 | 如果队列空,阻塞 |
阻塞队列方法分为以下3类,这取决于当队列满或空时它们的响应方式。如果将队列当作线程管理工具来使用,将要用到put和take方法。当试图向满的队列中添加或从空的队列中移出元素时,add、remove和element操作抛出异常。当然,在一个多线程程序中,队列会在任何时候空或满,因此,一定要使用offer、poll和peek方法作为替代。这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。
注释:poll和peek方法返回空来指示失败。因此,向这些队列中插入null值是非法的。
还有带有超时的offer方法和poll方法的变体。例如,调用boolean success = q.offer(x, 100, TimeUnit.MILLISECONDS);尝试在100毫秒的时间内在队列的尾部插入一个元素。如果成功返回true。否则,达到超时时,返回false。类似地,调用Object head = q.poll(100, TimeUnit.MILLISECONDS);尝试用100毫秒的时间移除队列的头元素;如果成功返回头元素,否则,达到超时时,返回null。
如果队列满,则put方法阻塞,如果队列空,则take方法阻塞。在不带超时参数时,offer和poll方法等效。
java.util.concurrent包提供了阻塞队列的几个变种。默认情况下LinkedBlockingQueue的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque是一个双端的版本。ArrayBlockingQueue在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数,那么等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在确实非常需要时才使用它。
PriorityBlockingQueue是一个带优先级的队列,而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限,但是,如果队列是空的,取元素的操作会阻塞。(有关优先级队列的详细内容参看第13章。)
最后,DelayQueue包含实现Delayed接口的对象:
interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
getDelay方法返回对象的残留延迟。负值表示延迟已经结束。元素只有在延迟用完的情况下才能从DelayQueue移除。还必须实现compareTo方法。DelayQueue使用该方法对元素进行排序。
例14-10中的程序展示了如何使用阻塞队列来控制线程集。程序在一个目录及它的所有子目录下搜索所有文件,打印出包含指定关键字的行。
例14-10 BlockingQueueTest.java
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
/**
* @version 1.0 2004-08-01
* @author Cay Horstmann
*/
public class BlockingQueueTest {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.print("Enter base directory (e.g. /usr/local/jdk1.6.0/src): ");
String directory = in.nextLine();
System.out.print("Enter keyword (e.g. volatile): ");
String keyword = in.nextLine();
final int FILE_QUEUE_SIZE = 10;
final int SEARCH_THREADS = 100;
BlockingQueue<File> queue = new ArrayBlockingQueue<File>(FILE_QUEUE_SIZE);
FileEnumerationTask enumerator = new FileEnumerationTask(queue, new File(directory));
new Thread(enumerator).start();
for (int i = 1; i <= SEARCH_THREADS; i++)
new Thread(new SearchTask(queue, keyword)).start();
}
}
/**
* This task enumerates all files in a directory and its subdirectories.
*/
class FileEnumerationTask implements Runnable {
/**
* Constructs a FileEnumerationTask.
*
* @param queue
* the blocking queue to which the enumerated files are added
* @param startingDirectory
* the directory in which to start the enumeration
*/
public FileEnumerationTask(BlockingQueue<File> queue, File startingDirectory) {
this.queue = queue;
this.startingDirectory = startingDirectory;
}
public void run() {
try {
enumerate(startingDirectory);
queue.put(DUMMY);
} catch (InterruptedException e) {
}
}
/**
* Recursively enumerates all files in a given directory and its
* subdirectories
*
* @param directory
* the directory in which to start
*/
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 static File DUMMY = new File("");
private BlockingQueue<File> queue;
private File startingDirectory;
}
/**
* This task searches files for a given keyword.
*/
class SearchTask implements Runnable {
/**
* Constructs a SearchTask.
*
* @param queue
* the queue from which to take files
* @param keyword
* the keyword to look for
*/
public SearchTask(BlockingQueue<File> queue, String keyword) {
this.queue = queue;
this.keyword = keyword;
}
public void run() {
try {
boolean done = false;
while (!done) {
File file = queue.take();
if (file == FileEnumerationTask.DUMMY) {
queue.put(file);
done = true;
} else
search(file);
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
}
}
/**
* Searches a file for a given keyword and prints all matching lines.
*
* @param file
* the file to search
*/
public void search(File file) throws IOException {
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();
}
private BlockingQueue<File> queue;
private String keyword;
}
生产者线程枚举在所有子目录下的所有文件并把它们放到一个阻塞队列中。这个操作很快,如果没有上限的话,很快就包含了所有找到的文件。
我们同时启动了大量搜索线程。每个搜索线程从队列中取出一个文件,打开它,打印所有包含该关键字的行,然后取出下一个文件。我们使用一个小技巧在工作结朿后终止这个应用程序。为了发出完成信号,枚举线程放置一个虚拟对象到队列中(这就像在行李输送带上放一个写着“最后一个包”的虚拟包)。当搜索线程取到这个虚拟对象时。将其放回并终止。
注意,不需要显式的线程同步。在这个应用程序中,我们使用队列数据结构作为一种间步机制。
API:java.util.concurrent.ArrayBlockingQueue<E> 5.0
-
ArrayBlockingQueue(int capacity)
-
ArrayBlockjngQueue(int capacity, boolean fair):构造一个带有指定的容量和公平性设置的阻塞队列。该队列用循环数组实现。
API:java.util.concurrent.LinkedB1ockingQueue<E> 5.0
API:java.util.concurrent.LinkedB1ockingDeque<E> 6
-
LinkedBlockingQueue()
-
LinkedBlockingDeque():构造一个无上限的阻塞队列或双向队列,用链表实现。
-
LinkedBlockingQueue(int capacity)
-
LinkedBlockfngDeque(int capacity):根据指定容量构建一个有限的阻塞队列或双向队列,用链表实现。
API:java.util.concurrent.DelayQueue<E extends Delayed> 5.0
-
DelayQueue():构造一个包含Delayed元素的无界的阻塞时间有限的阻塞队列。只有那些延迟已经超过时间的元素可以从队列中移出。
API:java.util.concurrent.Delayed 5.0
-
long getDelay(TimeUnit unit):得到该对象的延迟,用给定的时间单位进行度量。
API:java.util.concurrent.PriorityBlockingQueue<E> 5.0
-
PriorityBlockingQueue()
-
PriorityBlockingQueue(int initialCapacity)
-
PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator):构造一个无边界阻塞优先队列,用堆实现。参数:initialCapacity:优先队列的初始容量,默认值是11。comparator:用来对元素进行比较的比较器,如果没有指定,则元素必须实现Comparable接口。
API:java.util.concurrent.BlockingQueue<E> 5.0
-
void put(E element):添加元素,在必要时阻塞。
-
E take():移除并返回头元素,必要时阻塞。
-
boolean offer(E element, long time, TimeUnit unit):添加给定的元素,如果成功返回true。如果必要时阻塞,直至元素已经被添加或超时。
-
E poll(long time, TimeUnit unit):移除并返回头元素,必要时阻塞,直至元素可用或超时用完。失败时返回null。
API:java.util.concurrent.BlockingDeque<E> 6
-
void putFirst(E element)
-
void putLast{E element):添加元素,必要时阻塞。
-
E takeFirst()
-
E taKeLast():移除并返回头元素或尾元素,必要时阻塞。
-
boolean offerFirst(E element, long time, TimeUnit unit)
-
boolean offerLast(E element, long time, TimeUnit unit):添加给定的元素,成功时返回true,必要时阻塞直至元素被添加或超时。
-
f pollFirst(long time, TimeUnit unit)
-
E pollLast(long time, TimeUnit unit):移动并返回头元素或尾元素,必要时咀塞,直至元素可用或超时。失败时返回空。