POI多线程导出数据混乱的问题

在这里以07版excel导出为例,
问题描述:多线程导出excel,一个线程负责一个sheet的数据写入,出现数据混乱,可能会有大面积的重复数据,或者你设置的表头被设置到内容里去了。

1,出发点:
我有个需求(随便假设的),导出半个月的账单,每一天作为一个sheet。
先创建一个XSSFWork,然后分配多个线程,每个线程负责一个sheet,每个单元格分别写入行号跟列号

public static void main(String[] args){
        int days = 15;
        LocalDate today = LocalDate.now();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        XSSFWorkbook workbook = new XSSFWorkbook();
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        CountDownLatch cdl = new CountDownLatch(days);
        List<Future<Boolean>> list = IntStream.range(0,days).mapToObj(i->{
            String date = dtf.format(today.plusDays(i));
            XSSFSheet sheet = workbook.createSheet(date);
            Future<Boolean> future = executorService.submit(()->{
                try{
                    for(int j=0;j<100;j++){
                        XSSFRow row = sheet.createRow(j);
                        for(int k=0;k<100;k++){
                            row.createCell(k).setCellValue(j+"-"+k);
                        }
                    }
                    return true;
                }finally {
                    cdl.countDown();
                }
            });
            return future;
        }).collect(Collectors.toList());

        try{
            cdl.await(2,TimeUnit.MINUTES);

            for(Future<Boolean> future : list){
                future.get();
            }
            String path = "C:\\Users\\admin\\Desktop\\"+DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())+".xls";
            FileOutputStream os = new FileOutputStream(path);
            workbook.write(os);
            os.close();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }

运行生成文件截图示例:
在这里插入图片描述

2,原因在于SharedStringsTable这个类,在这里插入图片描述

需要注意的是,XSSFWork里面的sharedStringSource跟XSSFCell的_sharedStringSource两个相同类型的属性,其实都指向同一个对象,看XSSFCell的初始化就可以知道
在这里插入图片描述
因此只要是用的同一个XSSFWork,那么所有的这个变量其实都是指向了同一个实例。
现在讲下这个实例,看这个类的注释:
在这里插入图片描述
简单点说,就是一个excel里面的值重复率比较高,因此准备了这个类,每个唯一的字符串只会存一次,照这样设计,每个单元格,如果值相同,那么指向的就是这个实例里的一个唯一索引。
设计是好的,单线程跑也没问题,如果多线程,每个线程有自己的workBook,那也没问题,如果多线程,用了这同一个workbook,那就只有一份sharedStringsTable了,问题在这。
刚开始看,以为是这个类里的两个线程非安全的集合搞的鬼,在这里插入图片描述
一个是存每个唯一字符串转换出来的CTRstImpl对象,用的ArrayList
一个是存每个唯一字符串和对应在上述列表集合中的 索引之间的键值对,用的HashMap
二话不说,通过反射将这两个属性分别重新赋值
strings = new CopyOnWriteArrayList();
stmap = new ConcurrentHashMap<>();

编译运行,问题照样存在。

因此直接加日志,在setCellValue之后,马上取出来,两者值一比较,发现不对,点进去看setCellValue实现,在这里插入图片描述
这里再进去两层,到这里:
在这里插入图片描述
3,问题在这:
3.1,stmap.containsKey(s),完全有可能多个相同的字符串同时没通过这一关,从而代码执行到下面,stmap put两个相同的键,而后发的键值对覆盖了前一个键值对,但这两个相同的字符串经过处理都转换成CTRst存入strings,而这两个CTRst对象的索引很有可能被当做其他的stmap value值存到stmap了
3.2,获取strings的大小来作为唯一字符串的映射值,多线程并发时,有N个线程在插数据到strings,又有N个线程在获取strings的大小并写入stmap,导致唯一字符串对应的strings索引值可能不对

4,解决办法:
最好的办法是拿到POI源码,在ShardStringsTable.addEntry 方法上加上synchronized,我用的poi版本是4.0.0,我检查了调用setCellValue的时候,只有这里存在并发问题,使用到strings.size()的地方只有这里,使用到stmap.put的地方另有一处,是在读取输入流生成excel的时候用到的,这个就不会有冲突了,在这里插入图片描述
解决方案:
4.1,在setCellValue调用的时候,获取到sharedStringsTable实例,并以它为锁,执行setCellValue方法,保证setCellValue执行的时候,addEntry方法只能线程同步执行(需要将代码调用setCellValue统一封装在一个方法体内,并且锁的粒度稍微大了点,另外在这里通过反射从XSSFCell对象中获取到_sharedStringsTable会比较消耗性能,设置一个cell就用调一次反射,当然也可以在最顶层从XSSFwork中取到这个属性并一层层传过来)

public static void main(String[] args){
        int days = 15;
        LocalDate today = LocalDate.now();
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        XSSFWorkbook workbook = new XSSFWorkbook();
        SharedStringsTable sstLock = getSstLock(workbook);
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        CountDownLatch cdl = new CountDownLatch(days);
        List<Future<Boolean>> list = IntStream.range(0,days).mapToObj(i->{
            String date = dtf.format(today.plusDays(i));
            XSSFSheet sheet = workbook.createSheet(date);
            Future<Boolean> future = executorService.submit(()->{
                try{
                    for(int j=0;j<100;j++){
                        XSSFRow row = sheet.createRow(j);
                        for(int k=0;k<100;k++){
                            synchronized (sstLock){
                                row.createCell(k).setCellValue(j+"-"+k);
                            }
                        }
                    }
                    return true;
                }finally {
                    cdl.countDown();
                }
            });
            return future;
        }).collect(Collectors.toList());

        try{
            cdl.await(2,TimeUnit.MINUTES);

            for(Future<Boolean> future : list){
                future.get();
            }
            String path = "C:\\Users\\admin\\Desktop\\"+DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now())+".xls";
            FileOutputStream os = new FileOutputStream(path);
            workbook.write(os);
            os.close();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            executorService.shutdown();
        }
    }

    private static SharedStringsTable getSstLock(XSSFWorkbook workbook){
        try{
            Field field = workbook.getClass().getDeclaredField("sharedStringSource");
            field.setAccessible(true);
            return (SharedStringsTable)field.get(workbook);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

4.2,把源码拉过来,在SharedStringsTable.addEntry方法上加synchronized,改个版本号,编译构建打包,放maven私服去,并引用新的包

新的excel截图应该是这样:
在这里插入图片描述

备注:如果你用的是HSSFWork,也就是excel03版本的,那问题就是另外分析了,因为HSSFwork没有SharedStringsTable的设计

  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值