- 对于实际编程来说,应该尽可能远离底层结构。使用由并发处理的专业人士实现的较高层次的结构要方便得多,要安全得多。
- 对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。生产者线程向队列插入元素,消费者线程调用它们。使用队列,可以安全地从一个线程向另一个线程传递数据。例如,考虑银行转账程序,转账线程将转账指令对象插入一个队列中,而不是直接访问银行对象。另一个线程从队列中取出指令执行转账。只有该线程可以访问该银行对象的内部。因此不需要同步。
- 当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列导致线程阻塞。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。工作者线程可以周期性地将中间结果存储在阻塞队列中,其它的工作者线程移出中间结果并进一步加以修饰。队列会自动地平衡负载。如果第一个线程集运行得比第二个慢,第二个线程集在等待结果时会阻塞。如果第一个线程集运行得快,它将等待第二个线程集赶上来。
- 阻塞队列方法分为以下3类,这取决于当队列满或空时它们的响应方式。如果将队列当做线程管理工具来使用,将要用到
put
和take
。当试图向满的队列中添加或从空队列中移出元素时,add,remove和element操作抛出异常。当然,在一个多线程程序中,队列会在任何时候满,因此,一定要使用offer,poll和peek方法作为替代。这些方法如果不能完成任务,只是给出一个错误提示而不会抛出异常。 - 还有带有超时的offer方法和poll方法的变体。
blockingQueue.offer(x,100, TimeUnit.MILLISECONDS);
- 尝试在100毫秒的时间内在队列的尾部插入一个元素。如果成功返回true;否则,达到超时时,返回false。
- 类似地,下面的调用
Object head = blockingQueue.poll(100, TimeUnit.MILLISECONDS);
- 尝试用100毫秒的时间移出队列的头元素;如果成功返回头元素,否则,返回null。
java.util.concurrent
包提供了阻塞队列的几个变种。默认情况下,LinkedBlockingQueue
的容量是没有上边界的,但是,也可以选择指定最大容量。LinkedBlockingDeque
是一个双端版本。ArrayBlockingQueue
在构造时需要指定容量,并且有一个可选的参数来指定是否需要公平性。若设置了公平参数,则那么等待了最长时间的线程会优先得到处理。通常,公平性会降低性能,只有在确定非常需要时才使用它。PriorityBlockingQueue
是一个带有优先级的队列,而不是先进先出队列。元素按照它们的优先级顺序被移出。该队列是没有容量上限的,但是,如果队列是空的,取元素的操作会阻塞。- 以下的程序展示了如何使用阻塞队列来控制一组线程,程序在一个目录及它的所有目录下搜索所有文件,打印出包含指定关键字的行
public class BlockingQueueTest {
//文件队列的大小
private static final int FILE_QUEUE_SIZE = 10;
//搜索线程
private static final int SEARCH_THREADS =100;
//空文件,防止阻塞队列为空
private static final File DUMMY = new File("");
//创建ArrayBlockQueue阻塞队列,并指定文件队列的大小
private static BlockingQueue<File> queue = new ArrayBlockingQueue<>(FILE_QUEUE_SIZE);
public static void main(String[] args) {
try(Scanner in = new Scanner(System.in)){
System.out.println("Enter base directory (e.g /opt/jdk1.8.0/src)");
String directory = in.nextLine();
System.out.println("Enter keyword (e.g volatile):");
String keyword = in.nextLine();
Runnable enumerator = ()->{
try{
enumerate(new File(directory));
queue.put(DUMMY);
}catch (InterruptedException e){
e.printStackTrace();
}
};
new Thread(enumerator).start();
for(int i=1;i<=SEARCH_THREADS;i++){
Runnable searcher = ()->{
try{
boolean done = false;
while (!done){
File file = queue.take();
if(file == DUMMY){
queue.put(file);
done = true;
}
else search(file,keyword);
}
}catch (InterruptedException e) {
e.printStackTrace();
}catch (IOException e){
e.printStackTrace();
}
};
new Thread(searcher).start();
}
}
Runnable runnable =()->{
try {
sleep(10000);
File poll = queue.poll();
//这里可以看到空文件取出又放回
if(poll.equals(DUMMY)){
System.out.println("yes");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(runnable).start();
}
//递归枚举给定目录及其子目录中的所有文件
public static 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 void search(File file,String keyword)throws IOException{
//以"UTF-8"编码方式,读取文件
try(Scanner in = new Scanner(file,"UTF-8")){
//记录行号
int lineNumber = 0;
while (in.hasNext()){
lineNumber++;
String line = in.nextLine();
if(line.contains(keyword)){
System.out.printf("%s:%d:%s%n",file.getPath(),lineNumber,line);
}
}
}
}
}
- 我尝试使用
ArrayDeque
去替换BlockingQueue
,结果出现大量的java.lang.NullPointerException
。 - 生产线程枚举在所有子目录下的所有文件并把它们放在一个阻塞队列中。这个操作很快,如果没有上限的话,很快就包含了所有找到的文件。
- 我们同时启动了大量的搜索线程。每个搜索线程从队列中取出一个文件,打开它,打印所有包含该关键字的行,然后取出下一个文件。这里使用一个小技巧在工作结束后终止这个应用程序。为了发出完成信号,枚举线程放置一个虚拟对象到队列中(这就像在行里输送带上放一个写着"最后一个包"的虚拟包)。当搜索线程取到这个虚拟对象时,将其放回并终止。