80. ArrayBlockingQueue阻塞队列

ArrayBlockingQueue阻塞队列有一系列方法,主要区别在异常操作时的反应,持有一把全局锁。用于存储队列元素的存储空间是预先分配的,使用过程中内存开销较小,适合已知最大存储容量的场景。适用于生产消费模型,平衡两边的处理速度。

方法 行为 表现
add 添加一个元素 如果队列已满抛出异常 IllegalStateException
element 返回头部元素 如果队列为空抛出异常 NoSuchElementException
offer 添加一个元素 成功返回 true, 如果队列已满返回 false
peek 返回头部元素 如果队列为空,返回 null
poll 删除并返回该元素 如果队列为空返回 null
put 添加一个元素 如果队列已满则阻塞
remove 删除并返回该元素 如果队列为空抛出异常 NoSuchElementException
take 删除并返回该元素 如果队列为空则阻塞

下面为一个搜索特定目录下关键字存在的行:

package github.banana;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * 阻塞队列实例-搜索指定目录下文件中存在关键字的行
 */
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("");
    // 阻塞队列
    private static BlockingQueue<File> queue = new ArrayBlockingQueue<>(FILE_QUEUE_SIZE);

    /**
     * 搜索目录中存在关键字的行
     *
     * @param args args
     */
    public static void main(String args[]) {
        try (Scanner in = new Scanner(System.in)) {
            System.out.println("输入目录名 (e.g. /opt/jdk1.8.0/src)");
            String directory = in.nextLine();
            System.out.println("输入要搜索的关键字 (e.g. volatile)");
            String keyword = in.nextLine();

            Runnable r = () -> {
                try {
                    enumerate(new File(directory));
                    // 最后存入空文件标识队列处理完毕
                    queue.put(DUMMY);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
            new Thread(r).start();

            for (int i = 1; i <= SEARCH_THREADS; i++) {
                Runnable s = () -> {
                    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();
                    }
                };
                new Thread(s).start();
            }
        }
    }

    /**
     * 递归枚举目录下文件名和目录名
     *
     * @param directory 目录名
     */
    private static void enumerate(File directory) {
        // 列出目录下的所有文件和目录
        File[] files = directory.listFiles();
        try {
            // 文件可能存在空指针异常
            if (files == null) {
                System.out.println("错误:该目录内无任何文件或目录");
                return;
            }
            for (File file : files) {
                // 如果是目录,递归列出文件名
                if (file.isDirectory()) {
                    enumerate(file);
                } else {
                    // 把文件名存入队列中
                    // 当Queue已经满了时,会进入等待,只要不被中断,就会插入数据到队列中。会阻塞,可以响应中断。
                    queue.put(file);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 搜索文件内是否包含关键字
     *
     * @param file    文件名
     * @param keyword 待搜索关键字
     */
    private static void search(File file, String keyword) {
        // 按UTF-8编码方式读取文件内容
        try (Scanner in = new Scanner(file, "UTF-8")) {
            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);
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出表现:

输入目录名 (e.g. /opt/jdk1.8.0/src)
/Users/zhgxun/Documents/self/2018
输入要搜索的关键字 (e.g. volatile)
expect
/Users/zhgxun/Documents/self/2018/get_log.sh:5:# 该脚本的运行需要依赖脚本 auto_expect,将其部署到家目录$HOME下,比如/Users/baidu/auto_expect
/Users/zhgxun/Documents/self/2018/auto_expect:1:#!/usr/bin/expect
/Users/zhgxun/Documents/self/2018/auto_expect:3:# expect 进行交互式登录线上系统
/Users/zhgxun/Documents/self/2018/auto_expect:33:expect {
/Users/zhgxun/Documents/self/2018/auto_expect:39:        # 类似expect,不过是从标准输入读取字符,行必须以回车结尾,以使expect能识别它们。 
/Users/zhgxun/Documents/self/2018/auto_expect:40:        expect_user {
/Users/zhgxun/Documents/self/2018/auto_expect:46:        set TOKEN $expect_out(buffer)
/Users/zhgxun/Documents/self/2018/auto_expect:51:        # 允许expect继续执行自身而不是往下执行,默认情况下,exp_continue会重置timeout,如果不想重置timeout,使用-continue_timer选项。
/Users/zhgxun/Documents/self/2018/auto_expect:63:                expect -re "password:" { send "$PASSWORD\r" }
/Users/zhgxun/Documents/self/2018/get_log.sh:77:    # 使用expect方式依次登录机器,注意命令需要使用双引号包含起来避免被空格分割成单一参数
/Users/zhgxun/Documents/self/2018/get_log.sh:78:    /usr/bin/expect $HOME/auto_expect $USER_NAME $PIN ${HOST[$i]} "$CMD"
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:11:确实如此,有这么一个免费的编程工具语言有这种能完成用户交互的功能,它就是Expect。[百度百科](https://baike.baidu.com/item/expect/4598715#viewPageContent)这么描述:"用来实现自动和交互式任务进行通信,而无需人的干预。Expect的作者Don Libes在1990年开始编写Expect时对Expect做有如下定义:Expect是一个用来实现自动交互功能的软件套件(Expect [is a] software suite for automating interactive tools)。使用它,系统管理员可以创建脚本来对命令或程序进行输入,而这些命令和程序是期望从终端(terminal)得到输入,一般来说这些输入都需要手工输入进行的。Expect则可以根据程序的提示模拟标准输入提供给程序需要的输入来实现交互程序执行。"
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:34:该脚本我命名为auto_expect,并存放在家目录下$HOME/auto_expect,可任意命名,但要注意下一个文件中的可执行文件调用脚本应该与该处命名一致。
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:37:#!/usr/bin/expect
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:39:# expect 进行交互式登录线上系统
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:69:expect {
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:75:        # 类似expect,不过是从标准输入读取字符,行必须以回车结尾,以使expect能识别它们。 
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:76:        expect_user {
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:82:        set TOKEN $expect_out(buffer)
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:87:        # 允许expect继续执行自身而不是往下执行,默认情况下,exp_continue会重置timeout,如果不想重置timeout,使用-continue_timer选项。
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:99:                expect -re "password:" { send "$PASSWORD\r" }
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:119:下面这个文件我取名为get_log.sh,通过该脚本来执行来获取线上机器的日志信息。该脚本依赖上一个脚本auto_expect,需要注意路径一致。
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:126:# 该脚本的运行需要依赖脚本 auto_expect,将其部署到家目录$HOME下,比如/Users/baidu/auto_expect
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:198:    # 使用expect方式依次登录机器,注意命令需要使用双引号包含起来避免被空格分割成单一参数
/Users/zhgxun/Documents/self/2018/本地机器查看线上机器日志.md:199:    /usr/bin/expect $HOME/auto_expect $USER_NAME $PIN ${HOST[$i]} "$CMD"
/Users/zhgxun/Documents/self/2018/Java.md:2630:        long expectedSum = 0;
/Users/zhgxun/Documents/self/2018/Java.md:2633:            expectedSum += array[i];
/Users/zhgxun/Documents/self/2018/Java.md:2635:        System.out.println("Expected sum: " + expectedSum);

Process finished with exit code 0