记多线程读写Excel

425 阅读6分钟

需求:有x1,x2两个Excel表格,他们的内容格式相同,内容除第一列序号外,都为数字。除标题列他们只有一行数据,要求使用两个线程顺序交替向x3Excel表格中写入数据,第一次写入时数据内容不变,第二次及以后皆为原数据乘以2,且加一列时间戳数据。示例如下:

image.png

image.png

image.png

本文不具体实现该需求,而是通过该需求对多线程文件读写,进行实践,具体Excel内容处理逻辑不进行详细描述,使用伪代码进行表述。部分不规范代码还请指正,谢谢!

long startTime = System.currentTimeMillis();
AtomicInteger x1WriteCount = new AtomicInteger(0);//x1第几次写入
AtomicInteger x2WriteCount =  new AtomicInteger(0);//x2第几次写入
AtomicInteger x3RowNumber = new AtomicInteger(0);//x3写入行数 值减去1为序号
int needRownum = 1000;//写入数据行数,不包括标题
Semaphore x1Semaphore = new Semaphore(1);//x1写入信号量
Semaphore x2Semaphore= new Semaphore(0);//x2写入信号量
Semaphore firstSemaphore= new Semaphore(0);//第一次进入while循环前的锁
File file1 = new File("src/x1.xls");
File file2 = new File("src/x2.xls");
File file3 = new File("src/x3.xls");
//初始化x3
Workbook initWb = new HSSFWorkbook();
Sheet initSheet = initWb.createSheet("Sheet1");
initWb.write(new FileOutputStream(file3));
initWb.close();
// 定义日期格式 输出时间戳
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

//线程1负责x1的读取及写入,需要将第一行标题处理写入
Thread thread1= new Thread(()->{
    //若需不断写入则 设为true
    while(x3RowNumber.get()<needRownum){
        try {
            //交替写 同步信号量
            x1Semaphore.acquire();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //try with source  自动释放
        try(
            BufferedInputStream bufferedInputStream1 = new BufferedInputStream(new FileInputStream(file1));
            BufferedInputStream bufferedInputStream3= new BufferedInputStream(new FileInputStream(file3));
            Workbook workbook = new HSSFWorkbook(bufferedInputStream3);
            FileOutputStream fileOutputStream1 = new FileOutputStream(file3);
             ) {
                //最终输出sheet
                Sheet sheet = workbook.getSheetAt(0);
                HSSFWorkbook excel1 = new HSSFWorkbook(bufferedInputStream1);
                HSSFSheet sheet1 = excel1.getSheetAt(0);  //获取第一个sheet页(下标从0开始)
                //创建Excel工作簿对象(读取Excel文件)
                int rowNumber = sheet1.getPhysicalNumberOfRows();//获取sheet页中行数
                //因处理逻辑实现的比较繁琐,故省略Excel文件数据处理 ......
                //添加一行Excel到x3
                x3RowNumber.getAndIncrement();
                }
                    workbook.write(fileOutputStream1);
                    //第一次写完后释放锁
                    firstSemaphore.release();
                    System.out.println("x1写入完成"+x1WriteCount.get());
                    System.out.println("x3行数"+x3RowNumber.get());
                    x1WriteCount.getAndIncrement();            
                    x2Semaphore.release();
            }
            catch (Exception e) {
                 e.printStackTrace();
            }
    }
});
Thread thread2= new Thread(()->{
    try {
        //防止过早进入while循环 序号错位
        firstSemaphore.acquire();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    while(x3RowNumber.get()<needRownum){
        try {
            x2Semaphore.acquire();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    try (
            BufferedInputStream bufferedInputStream2= new BufferedInputStream(new FileInputStream(file2));
            BufferedInputStream bufferedInputStream3= new BufferedInputStream(new FileInputStream(file3));
            Workbook workbook = new HSSFWorkbook(bufferedInputStream3);
            FileOutputStream fileOutputStream2 = new FileOutputStream(file3);
        )
        {
                Sheet sheet = workbook.getSheetAt(0);
                HSSFWorkbook excel2 = new HSSFWorkbook(bufferedInputStream2);
                //创建Excel工作簿对象(读取Excel文件)
                HSSFSheet sheet2 = excel2.getSheetAt(0);  //获取第一个sheet页(下标从0开始)
                int rowNumber = sheet2.getPhysicalNumberOfRows();//获取sheet页中行数
                //因处理逻辑实现的比较繁琐,故省略Excel文件数据处理 ...... 
                //添加一行Excel到x3
                x3RowNumber.getAndIncrement();
        }
                workbook.write(fileOutputStream2);
                System.out.println("x2写入完成"+x2WriteCount.get());
                System.out.println("x3行数"+x3RowNumber.get());
                x2WriteCount.getAndIncrement();
                x1Semaphore.release();
        }
         catch (Exception e) {
        e.printStackTrace();
    }
    }
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime+"ms");

因该代码最初实现时,两个线程的具体实现逻辑不同(处理标题列等原因),所以没有抽取出单独的类来实现。实现后发现其实可以抽取成单独的类进行实现,但该DEMO主要用于进行多线程文件读写实践,故不去再做修改。在需求完成过程中,主要遇到过的问题有:
对Excel文件理解不足,在追加的时候一直尝试通过在FileOutputStream创建时,使用追加模式进行Excel表格的追加。实际上这种做法只会覆盖原Excel,所以在写完一行后,需重新读取Excel,再在原Excel基础上对数据进行追加,再重新整个写入。观察EaxyExcel.write()方法后,以目前个人的水平看来似乎也是重新读取这么做的。

image.png image.png
实现完成基本需求后,尝试对代码进行优化。
1、下面这段代码中,try with source 会自动关闭流,但每次线程切换时会重新打开流再次读取一遍X1,个人认为会影响性能,故进行优化,将读取X1的流,拿到while循环外避免io流的重复开关。但实际测试下来,插入1000行的数据,两种做法优化后13952ms,优化前14256ms,相差200-300ms,貌似差距不是很大。

 while(x3RowNumber.get()<needRownum){
        try {
            //交替写 同步信号量
            x1Semaphore.acquire();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        try(
            BufferedInputStream bufferedInputStream1 = new BufferedInputStream(new FileInputStream(file1));
            BufferedInputStream bufferedInputStream3= new BufferedInputStream(new FileInputStream(file3));
            Workbook workbook = new HSSFWorkbook(bufferedInputStream3);
            FileOutputStream fileOutputStream1 = new FileOutputStream(file3);
            )
             .....
         }
HSSFWorkbook excel1 = null;
HSSFSheet sheet1=null;
try(BufferedInputStream bufferedInputStream1 = new BufferedInputStream(new FileInputStream(file1));
    ){
    excel1 = new HSSFWorkbook(bufferedInputStream1);
    sheet1 = excel1.getSheetAt(0);
}catch (IOException e) {
    throw new RuntimeException(e);
}
 while(x3RowNumber.get()<needRownum){
        try {
            //交替写 同步信号量
            x1Semaphore.acquire();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

第一次尝试使用IDEA的Profiler进行分析,粗略的观察最主要的耗时在为了实现同步而等待信号量的时候,以及重新读取X3时。

image.png

这里在我的理解中因需求需要同步,等待信号量的耗时应该无法优化,之后我会尝试其他方法看是否能达成同步。目前的理解中不知道Java中有哪些实际的解决方案,若有希望能指教一下。我只能推测一下,通过信号量等方式同步会进行加锁,从而影响性能,那么用Synchronized以及ReenterLock等应该性能差距不大,不知道能否使用队列等方式,通过队列FIFO的性质实现顺序同步向X3中插入数据。之后将去了解一下Java中并发时使用队列来实现同步的方法,这种方式应该会比加锁效率更高。
重新写了一个DEMO不是操作EXCEL表格而是操作TXT文件,发现其实信号量同步等待所消耗的时间取决于文件操作的时间,同时Java中的这些并发类,Semaphore,ReentrantLock,CountDownLatch,CyclicBarrier等都是基于AQS实现,也就是使用了队列来实现同步。

image.png 而读取X3花费的大量时间,之后将去了解Java中是否有更加高效的IO实现方式来优化读取时间。
更新:重新对代码逻辑进行梳理后发现可以避免反复读取X3文件,在最开始时初始化了一个WorkBook对象,两个线程对该对象轮流进行数据修改添加,在手动关闭该对象前,他是一直存在的。对代码进行以下修改

image.png 将上图中的workbook对象替换成最初初始化的那一个workbook对象。替换后try with source中的读取X3以及重新读取workbook也可以删去,写入时使用初始化的initworkbook对象写入。

image.png

image.png

image.png
这样处理后,写入1000行数据的时间由14s左右降到了1.4s。
image.png
此时写入的逻辑为initworkbook写入一行数据就在所在线程写入文件一次,若修改成1000行一次性写入,速度将来到400ms左右。

image.png image.png image.png
同时了解到Excel文件可以转换成CSV格式文件,CSV格式为纯文本格式,如果以这种方式来进行导出可能效率会更高,有待尝试。