在这里以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的设计