一种excel多线程并发写sheet的方案

一、背景

        有一次项目的需求要求导出excel,并且将不同的数据分别写到不同的sheet中。

二、 方案概述

        首先一开始使用easyexcel去导出excel,结果发现导出时间需要3秒左右。于是想着能不能缩短excel导出时间,于是第一次尝试使用异步线程去查询数据库,却发现接口的时间并没有明显缩短,于是自己就开始排查耗时的操作,于是发现是写sheet的时候是串行执行,并且每个写sheet的时间并不短,尤其在sheet比较多的时候,会导致导出的时间比较长。于是,想着能不能使用异步线程并发去写sheet,但是,使用的时候报错。后来去找报错的原因,是因为easyexcel并不支持并发写。于是,我就转战POI。尝试是否能够并发写入多个sheet。

使用easyexcel写入多个sheet

        try {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            response.setCharacterEncoding("utf-8");
            // 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
            String fileName = URLEncoder.encode(EXCEL, "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
            AtomicInteger atomicInteger = new AtomicInteger(0);
            ExcelWriter build = EasyExcel.write(response.getOutputStream(),VirtualInstanceDataPoint.class).build();
            list.stream().map(i -> CompletableFuture.supplyAsync(() -> {
                        return service.list();
                    }, executor)).collect(Collectors.toList()).stream()
                    .map(CompletableFuture::join).collect(Collectors.toList()).forEach(r->{
                        int andIncrement = atomicInteger.getAndIncrement();
                        WriteSheet build1 = EasyExcel.writerSheet(andIncrement, r.get(0).getDevice() + andIncrement).build();
                        build.write(r, build1);
                    });
            build.finish();
            response.flushBuffer();
        } catch (Exception e) {
            // 重置response
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().println(JSON.toJSONString(R.error().message(e.getMessage()).code(20007)));
}

并发写入多个sheet会报

org.apache.poi.openxml4j.exceptions.PartAlreadyExistsException: A part with the name '/xl/worksheets/sheet1.xml' already exists : Packages shall not contain equivalent part names and package implementers shall neither create nor recognize packages with equivalent part names. [M1.12]

 POI写入多个sheet

    String[] EXPORT_HEADER = {"head1","head2"};
    @GetMapping("export3")
    @ApiOperation(value = "excel导出信息")
    @SneakyThrows
    public void export3(HttpServletResponse response) {
        OutputStream outputStream = response.getOutputStream();
        response.reset();
        response.setContentType("application/vnd.ms-excel");
        response.setHeader("Content-disposition", "attachment;filename=template.xls");
        AtomicInteger atomicInteger = new AtomicInteger();
        HSSFWorkbook workbook = new HSSFWorkbook();
        Font font = workbook.createFont();
        font.setBold(true);
        HSSFCellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        cellStyle.setFont(font);
        List<IndexData> indexDatas = new ArrayList<>();
        indexDatas.add(new IndexData("1",1.1));
        indexDatas.add(new IndexData("2",2.2));
        indexDatas.add(new IndexData("3",3.3));
        for (IndexData indexData : indexDatas) {
            HSSFSheet sheet = workbook.createSheet(indexData.getStr());
            HSSFRow row = sheet.createRow(0);
            // 创建表头
            for (int i = 0; i < EXPORT_HEADER.length; i++) {
                HSSFCell cell = row.createCell(i);
                cell.setCellValue(EXPORT_HEADER[i]);
                cell.setCellStyle(cellStyle);
            }
            row = sheet.createRow(1);
            row.createCell(0).setCellValue(indexData.getStr());
            row.createCell(1).setCellValue(indexData.getDoubleData());
        }

        workbook.write(outputStream);
        outputStream.flush();
        outputStream.close();
    }
    static  class IndexData {
        public IndexData(String string, Double doubleData) {
            this.str = string;
            this.doubleData = doubleData;
        }

        public String getStr() {
            return str;
        }

        public Double getDoubleData() {
            return doubleData;
        }

        private String str;

        private Double doubleData;
    }

POI多线程写多个sheet

    String[] EXPORT_HEADER = {"head1","head2"};
    @GetMapping("export3")
    @ApiOperation(value = "excel导出")
    @SneakyThrows
    public void export3(HttpServletResponse response) {
        OutputStream outputStream = response.getOutputStream();
        response.reset();
        response.setContentType("application/vnd.ms-excel");
        response.setHeader("Content-disposition", "attachment;filename=template.xls");
        AtomicInteger atomicInteger = new AtomicInteger();
        HSSFWorkbook workbook = new HSSFWorkbook();
        Font font = workbook.createFont();
        font.setBold(true);
        HSSFCellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        cellStyle.setFont(font);
        List<IndexData> indexDatas = new ArrayList<>();
        indexDatas.add(new IndexData("1",1.1));
        indexDatas.add(new IndexData("2",2.2));
        indexDatas.add(new IndexData("3",3.3));
        indexDatas.stream().map(data ->CompletableFuture.runAsync(() ->{
            HSSFSheet sheet = workbook.createSheet(data.getStr());
            HSSFRow row = sheet.createRow(0);
            // 创建表头
            for (int i = 0; i < EXPORT_HEADER.length; i++) {
                HSSFCell cell = row.createCell(i);
                cell.setCellValue(EXPORT_HEADER[i]);
                cell.setCellStyle(cellStyle);
            }
            row = sheet.createRow(1);
            row.createCell(0).setCellValue(data.getStr());
            row.createCell(1).setCellValue(data.getDoubleData());
        })).collect(Collectors.toList()).stream().map(CompletableFuture::join).collect(Collectors.toList());
        workbook.write(outputStream);
        outputStream.flush();
        outputStream.close();
    }
    static  class IndexData {
        public IndexData(String string, Double doubleData) {
            this.str = string;
            this.doubleData = doubleData;
        }

        public String getStr() {
            return str;
        }

        public Double getDoubleData() {
            return doubleData;
        }

        private String str;

        private Double doubleData;
    }

但是有时候会报错

java.lang.IllegalArgumentException: calculated end index (2576) is out of allowable range (2564..2569)

是因为在 子线程中创建sheet产生并发。

一个解决方案是主线程预先创建sheet

另一个方案是为创建sheet的操作加锁

    @GetMapping("export1")
    @ApiOperation(value = "excel导出信息")
    @SneakyThrows
    public void export2(HttpServletResponse response) {
        OutputStream outputStream = response.getOutputStream();
        response.reset();
        response.setContentType("application/vnd.ms-excel");
        response.setHeader("Content-disposition", "attachment;filename=template.xls");
        AtomicInteger atomicInteger = new AtomicInteger();
        HSSFWorkbook workbook = new HSSFWorkbook();
        Font font = workbook.createFont();
        font.setBold(true);
        HSSFCellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        cellStyle.setFont(font);
        list.stream().map(instanceId -> CompletableFuture.runAsync(() -> {
                    List<> collect =  service.list();
                    HSSFSheet sheet = workbook.createSheet(collect.get(0).getDevice()+ atomicInteger.getAndIncrement());
                    HSSFRow row = sheet.createRow(0);
                    // 创建表头
                    for (int i = 0; i < EXPORT_HEADER.length; i++) {
                        HSSFCell cell = row.createCell(i);
                        cell.setCellValue(EXPORT_HEADER[i]);
                        cell.setCellStyle(cellStyle);
                    }
                    for (int i = 0; i < collect.size(); i++) {
                        row = sheet.createRow(i + 1);
                        row.createCell(0).setCellValue(collect.get(i).getInstanceId());
                        row.createCell(1).setCellValue(collect.get(i).getDevice());
                        row.createCell(2).setCellValue(collect.get(i).getDataId());
                        row.createCell(3).setCellValue(collect.get(i).getDataName());
                    }
                }, executor)).collect(Collectors.toList()).stream()
                .map(CompletableFuture::join).collect(Collectors.toList());
        workbook.write(outputStream);
        outputStream.flush();
        outputStream.close();
        
    }

以下使用加锁方式

    String[] EXPORT_HEADER = {"head1","head2"};
    @GetMapping("export3")
    @ApiOperation(value = "excel导出信息")
    @SneakyThrows
    public void export3(HttpServletResponse response) {
        OutputStream outputStream = response.getOutputStream();
        response.reset();
        response.setContentType("application/vnd.ms-excel");
        response.setHeader("Content-disposition", "attachment;filename=template.xls");
        AtomicInteger atomicInteger = new AtomicInteger();
        HSSFWorkbook workbook = new HSSFWorkbook();
        Font font = workbook.createFont();
        font.setBold(true);
        HSSFCellStyle cellStyle = workbook.createCellStyle();
        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        cellStyle.setFont(font);
        List<IndexData> indexDatas = new ArrayList<>();
        indexDatas.add(new IndexData("1",1.1));
        indexDatas.add(new IndexData("2",2.2));
        indexDatas.add(new IndexData("3",3.3));
        indexDatas.stream().map(data ->CompletableFuture.runAsync(() ->{
            HSSFSheet sheet = getSheet(data, workbook);
            HSSFRow row = sheet.createRow(0);
            // 创建表头
            for (int i = 0; i < EXPORT_HEADER.length; i++) {
                HSSFCell cell = row.createCell(i);
                cell.setCellValue(EXPORT_HEADER[i]);
                cell.setCellStyle(cellStyle);
            }
            row = sheet.createRow(1);
            row.createCell(0).setCellValue(data.getStr());
            row.createCell(1).setCellValue(data.getDoubleData());
        })).collect(Collectors.toList()).stream().map(CompletableFuture::join).collect(Collectors.toList());
        workbook.write(outputStream);
        outputStream.flush();
        outputStream.close();
    }

    @org.jetbrains.annotations.NotNull
    private synchronized static HSSFSheet getSheet(IndexData data, HSSFWorkbook workbook) {
        HSSFSheet sheet = workbook.createSheet(data.getStr());
        return sheet;
    }

但是这种方式还是会有一些并发带来的错误。

java.lang.NullPointerException: null
    at org.apache.poi.hssf.record.SSTSerializer.serialize(SSTSerializer.java:70)
    at org.apache.poi.hssf.record.SSTRecord.serialize(SSTRecord.java:279)
    at org.apache.poi.hssf.record.cont.ContinuableRecord.getRecordSize(ContinuableRecord.java:60)
    at org.apache.poi.hssf.model.InternalWorkbook.getSize(InternalWorkbook.java:1072)
    at org.apache.poi.hssf.usermodel.HSSFWorkbook.getBytes(HSSFWorkbook.java:1474)
    at org.apache.poi.hssf.usermodel.HSSFWorkbook.write(HSSFWorkbook.java:1386)
    at org.apache.poi.hssf.usermodel.HSSFWorkbook.write(HSSFWorkbook.java:1374)

但是在本机实测100个线程10个循环出错的个数在20-30之间。

我们可以捕获这些错误使用do while循环,当出错的时候可以清空状态再次重试。

1、重试机制一定要给重试次数,不然出现意外的错误,会导致死循环

总结:

        该方法只是本菜鸡的愚见,使用这种方式的确可以完成并发写sheet,缩短接口的相应速度,将3秒左右的接口降低到50ms左右。应该能适合sheet略多,但并发量、数据量不多的excel导出,但本人也是第一次使用POI,所以可能有错误,希望大佬们能够多多指点。

  • 40
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在Java中实现多线程并发可以使用Thread类或Executor框架。 以下是使用Thread类的示例: ``` public class MyThread extends Thread { @Override public void run() { // 执行线程任务 } } public class Main { public static void main(String[] args) { MyThread thread1 = new MyThread(); MyThread thread2 = new MyThread(); // 启动线程 thread1.start(); thread2.start(); } } ``` 以下是使用Executor框架的示例: ``` import java.util.concurrent.Executor; import java.util.concurrent.Executors; public class Main { public static void main(String[] args) { Executor executor = Executors.newFixedThreadPool(2); Runnable task1 = new Runnable() { @Override public void run() { // 执行线程任务1 } }; Runnable task2 = new Runnable() { @Override public void run() { // 执行线程任务2 } }; // 启动线程 executor.execute(task1); executor.execute(task2); } } ``` ### 回答2: 在Java中,通过使用多线程可以实现并发操作。下面是一个用Java编多线程并发的示例: ```java public class MultiThreadExample { public static void main(String[] args) { // 创建并启动多个线程 Thread thread1 = new MyThread("Thread 1"); Thread thread2 = new MyThread("Thread 2"); Thread thread3 = new MyThread("Thread 3"); thread1.start(); thread2.start(); thread3.start(); } static class MyThread extends Thread { private String name; public MyThread(String name) { this.name = name; } @Override public void run() { // 线程需要执行的任务 for (int i = 1; i <= 5; i++) { System.out.println(name + " 执行第 " + i + " 步"); // 睡眠一段时间模拟执行过程中的耗时操作 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } } ``` 上述示例中,创建了三个自定义线程`MyThread`的实例,并分别传递了不同的名称。然后,通过调用`start()`方法来启动线程。 每个线程在`run()`方法中通过使用`for`循环模拟了执行5个步骤的操作。在每个步骤中,线程会打印出其名称和步骤数,并且在每次步骤之间会暂停500毫秒以模拟耗时操作。 由于多个线程同时执行,所以它们会并发地执行各自的任务,从而实现了多线程并发操作。 ### 回答3: 在Java中,可以使用多种方式实现多线程并发。以下是一种常见的方法: 首先,需要定义一个类来表示线程。这个类必须继承Thread类,并实现其run方法。在run方法中,可以编需要线程执行的代码。 接下来,可以创建多个线程的实例,并调用它们的start方法,以启动线程。每个线程将执行自己定义的run方法中的代码。 在多线程并发中,需要特别注意的是共享资源的并发访问。由于多个线程可能同时访问共享资源,如果不进行合理的同步控制,可能会导致数据出错或死锁等问题。 为了避免这种情况,可以使用Java提供的synchronized关键字来实现同步。对于需要同步访问的代码块,可以使用synchronized关键字来修饰,确保同时只有一个线程能够执行此代码块。 此外,也可以使用Lock和Condition等Java中提供的同步工具类来实现更为灵活和精确的同步控制。 总结来说,编一个多线程并发程序的基本步骤为:定义线程类,实现run方法,创建线程实例并启动,合理进行同步控制。通过这些步骤,可以实现多线程并发执行,充分发挥计算资源的利用率,提高程序的执行效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值