Java8 API文档搜索引擎_优化构建索引速度

本专栏前文已介绍完成索引模块程序:

https://blog.csdn.net/m0_63299495/article/details/157515700?spm=1011.2415.3001.5331https://blog.csdn.net/m0_63299495/article/details/157515700?spm=1011.2415.3001.5331并对关键部分进行了细节整理:

https://blog.csdn.net/m0_63299495/article/details/157516644?spm=1011.2415.3001.5331https://blog.csdn.net/m0_63299495/article/details/157516644?spm=1011.2415.3001.5331本文介绍优化点:优化构建索引速度。


目录

[1. 验耗时](#1. 验耗时)

[2. 多线程制作索引](#2. 多线程制作索引)

[2.1 关于CountDownLatch](#2.1 关于CountDownLatch)

[2.2 关于线程加锁](#2.2 关于线程加锁)

[2.3 关于守护线程](#2.3 关于守护线程)

[3. 修改后的Parser类的run方法](#3. 修改后的Parser类的run方法)


1. 验耗时

在Paser类中的run方法内打时间戳检验耗时操作:

java 复制代码
public void run(){
        long beg = System.currentTimeMillis();
        System.out.println("开始构造索引");

        long Beg = System.currentTimeMillis();
//        1、根据加载文档路径,枚举该路径目录及其子目录下的所有文件(html)
        ArrayList<File> fileList=new ArrayList<>();
        // INPUT-PATH表示开始进行递归遍历的起始目录
        // fileList表示递归遍历的结果
        enumFile(INPUT_PATH, fileList);
        long enumFileEnd = System.currentTimeMillis();
        System.out.println("枚举文件耗时:"+(enumFileEnd-Beg)+" ms");

//        2、根据罗列出的文件路径打开文件,读取文件内容,进行解析并构建索引

        for(File f: fileList){
//            parseHTML方法用于解析单个HTML文件
            System.out.println("开始解析 "+f.getAbsolutePath());
            parseHTML(f);
        }
        long forEnd = System.currentTimeMillis();
        System.out.println("遍历文件耗时:"+(forEnd-enumFileEnd)+" ms");
//        3、把内存中构造的索引数据结构保存到指定文件中
        index.save();
        System.out.println("完成构造索引");
        long end = System.currentTimeMillis();
        System.out.println("构建索引耗时:"+(end - beg)+" ms ");
    }

再次启动Paser类,获取耗时如下:

(省略中间各html文件的遍历输出结果)

可见在构建索引的枚举、遍历和保存三个操作中,遍历文件耗时占比最高,现基于该问题进行制作索引的优化,考虑使用多线程制作索引。

2. 多线程制作索引

2.1 关于CountDownLatch

CountDownLatch是一个同步辅助工具,允许一个或多个线程等待其他线程完成操作,其实现线程同步的思想是计数器思想,countDown方法实现减少计数器值,await方法实现等待计数器清零

通过submit向线程池里提交任务,只是把Runnable对象放到阻塞队列中,并不代表线程池中的文档在submit提交完成后也被全部解析完了。为了保证执行save时保存的是完整的解析后的全部文档,采用CountDownLatch的await方法来表示所有任务都完成。

2.2 关于线程加锁

在Index类中有一个addDoc方法,会调用buildForward方法和buildInverted方法,buildForward方法会修改forwardIndex,buildInverted方法会修改invertedIndex,四个线程并发调用addDoc时就存在线程安全问题,需要加锁来解决线程安全问题。

如果直接把synchronized加到parseHTML或addDoc上,加锁粒度太粗使得并发程度较低,需要再细致地考虑加锁的粒度。

在buildForwad方法中,设置docId和将新doc插入到正排索引中两个操作需要加锁:

java 复制代码
        synchronized (locker1){
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }

在buildInverted方法中,在倒排拉链中根据关键词去倒排索引中查找的结果的操作都需要加锁:

java 复制代码
 synchronized (locker2){
                List<Weight> invertedList = invertedIndex.get(entry.getKey());
                // 如果为空则插入新键值对
                if(invertedList == null){
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    // 把当前的文档信息docInfo构造成Weight对象
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 假定权重公式:标题中出现的次数*10+正文中出现的次数*1
                    weight.setWeight(entry.getValue().titleCount*10+entry.getValue().contentCount);
                    newInvertedList.add(weight);
                    invertedIndex.put(entry.getKey(),newInvertedList);
                }else{
                    //非空则将当前文档信息docInfo构造成Weight对象插入倒排拉链
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().titleCount*10+entry.getValue().contentCount);
                    invertedList.add(weight);
                }
            }

且注意二者并不竞争同一锁资源,故创建的locker1和locker2为不同锁资源:

java 复制代码
    // 新创建两个锁对象
    private Object locker1 = new Object();
    private Object locker2 = new Object();

2.3 关于守护线程

如果一个线程是守护线程(后台线程),则这个线程的运行状态不会影响到进程结束。

如果一个线程不是守护线程,则这个线程的运行状态就会影响到进程结束。

之前我们采用的是线程池创建线程:

java 复制代码
 ExecutorService executorService = Executors.newFixedThreadPool(4);

默认创建出来的都是非守护线程,故当main方法执行完后这些线程仍然在等待新任务,并未终止,需要使用shutdown方法进行手动终止。

可见使用多线程后,构建索引耗时由17s将至7s,效率得到了提升。

3. 修改后的Parser类的run方法

java 复制代码
 /*
    * 优化制作索引:多线程制作索引
    * */
    public void run() throws InterruptedException {
        long beg = System.currentTimeMillis();
        System.out.println("开始构建索引");
        // 1. 枚举文件:
        ArrayList<File> files = new ArrayList<>();
        enumFile(INPUT_PATH, files);
        // 2. 多线程循环遍历文件:
        CountDownLatch latch = new CountDownLatch(files.size());
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for(File f: files){
            // 通过submit向线程池里提交任务,只是把Runnable对象放到阻塞队列中
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("解析: "+f.getAbsolutePath());
                    parseHTML(f);
                    latch.countDown();
                }
            });
        }
        // 3. 待所有文件解析完成后再保存索引:
        latch.await();
        // 手动终止线程池中的所有线程
        executorService.shutdown();
        index.save();

        long end = System.currentTimeMillis();
        System.out.println("完成构建索引");
        System.out.println("构建索引耗时:"+(end-beg)+" ms");
    }
相关推荐
北凉军2 小时前
IDEA中热部署插件JRebel激活失败404
java·ide·intellij-idea
乐观甜甜圈2 小时前
在Windows系统上hprof文件是否可以删除
java
野犬寒鸦2 小时前
从零起步学习并发编程 || 第二章:多线程与死锁在项目中的应用示例
java·开发语言·数据库·后端·学习
张np2 小时前
java进阶-Zookeeper
java·zookeeper·java-zookeeper
long3162 小时前
合并排序 merge sort
java·数据结构·spring boot·算法·排序算法
杜子不疼.2 小时前
内网监控工具翻身!Uptime Kuma+cpolar 实现远程运维自由
linux·运维·服务器
Lisson 32 小时前
VF01修改实际开票数量增强
java·服务器·前端·abap
WZTTMoon2 小时前
【无标题】
java
拾光Ծ2 小时前
【Linux】Ext系列文件系统(一):初识文件系统
linux·运维·服务器·硬件架构·ext文件系统