本文主旨:
- 基于SXSSFWorkbook将百万数据插入Excel;
- 多个Sheet页采用线程池的方式并发插入数据;
- ThreadPoolExecutor线程池实现;
- 基于CountDownLatch实现线程多线程结束后进行下一步操作;
- 基于Java VisualVM排查内存溢出并且分析POI窗口参数;
- 重温GC过程;
先介绍我们的Demo要实现的功能:
【创建一个四线程的线程池,将一个包含12个sheet页的2007版Excel快速填满数据。其中两个sheet页数据达到100万。】
技术细节以注释形式讲解:
//定义一个常量,也就是sheet页的数量,等同于线程池里循环调用的次数。
private final static int sheetDealCount = 12;
public static void main(String[] args) throws IOException {
//定义一个CountDownLatch,它的作用可以理解为集齐七龙珠召唤神龙。
//实际本例中我们必须要等线程池的所有线程执行完写数据的任务最后执行write操作。
//在count到0之前,主线程将一直await状态。
final CountDownLatch countDownLatch = new CountDownLatch(sheetDealCount);
long startTime = System.currentTimeMillis();
//我们定义一个窗口是100000大小的SXSSFWorkbook
//这里所谓的窗口是指n条数据写到内存后,直接转移到磁盘中,从而节约内存空间
SXSSFWorkbook sw = new SXSSFWorkbook(100000);
for(int i=1;i<=12;i++){
swList.add(sw.createSheet("s"+i));
}
//创建线程池,最大容纳4条线程
ThreadPoolExecutor tpe = new ThreadPoolExecutor(4, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(12));
try{
for(SXSSFSheet sxssfSheet : swList){
//创建线程对象,用于执行写Excelsheet页方法
ExportHVThreadtest et = new ExportHVThreadtest(sxssfSheet, sxssfSheet.getSheetName().replace("s", "").toString());
tpe.execute(() -> {
//执行写excel方法
et.run();
//每执行完一个线程方法,count数减一。
countDownLatch.countDown();
});
}
}catch(Exception e){
e.printStackTrace();
}finally{
tpe.shutdown();
}
try {
OutputStream out = new FileOutputStream("F:/test/test.xlsx");
//只要countDown的数量大于0,那么主线程将一直处于阻塞状态
countDownLatch.await();
//一旦所有线程执行完毕,及执行write方法。所谓的write就是完成excel的数据插入操作。
sw.write(out);
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime);
}
下面是写Sheet的线程类代码,demo中一共分12个sheet,每个sheet 多至100万行,并且拥有14列(按照我的业务场景创建的测试数据)
/**
* @author ljh
*/
public class ExportHVThreadtest extends Thread{
private SXSSFSheet sheet; //sheet实体
private String sheetCode; //sheet名
public ExportHVThreadtest(SXSSFSheet sheet, String sheetCode) {
this.sheet = sheet;
this.sheetCode = sheetCode;
}
public void run() {
System.out.println(Thread.currentThread().getName() + " " + sheetCode);
//区分1-12Sheet编号
switch(sheetCode){
case "1":
for(int i=0;i<100;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "2" :
for(int i=0;i<1000000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "3" :
for(int i=0;i<1000000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "4" :
for(int i=0;i<1000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "5" :
for(int i=0;i<10000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "6" :
for(int i=0;i<10000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "7" :
for(int i=0;i<10000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "8" :
for(int i=0;i<10000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "9" :
for(int i=0;i<10000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "10" :
for(int i=0;i<10000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "11" :
for(int i=0;i<10000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
case "12" :
for(int i=0;i<10000;i++){
SXSSFRow sxssfRow = sheet.createRow(i);
for(int j=0;j<14;j++){
sxssfRow.createCell(j).setCellValue("(" + i + "," + j + ")");
}
System.out.println("sheet:"+sheetCode+" row:"+i);
}
break;
default:
break;
}
}
}
以上内容将会满足您使用多线程的方式快速构建大数据量的Excel。
但是在构建过程中会遇到一些问题,解决问题的历程才是也分享给大家
1、窗口大小的设定
众所周知XSSFWorkbook是非常耗费内存的,我测试过大约20列30w行数据即可让jvm内存溢出崩溃掉。
于是poi带给了我们SXSSFWorkbook,用好这个实现的关键就是用好窗口,窗口在它的构造函数中体现。如果您选择窗口参数为-1那么代表所有数据将会缓存到内存,最后统一刷入磁盘。但是建议不要这样做,因为他也会导致老年代频繁GC,从而因为GC时间过长导致OOM。
2、如何判断内存消耗
简单介绍一下Java VisualVM工具,他是jdk自带的一款免费jvm图形化统计分析工具,在这个工具中我们需要下载一个Visual GC插件:https://visualvm.github.io/pluginscenters.html
登陆上面的网址下载适合自己jdk版本的插件
第一步:打开CMD,输入jvisualvm,
第二步 点击工具
点击工具->插件->已下载->添加插件 : 将刚才下载好的插件添加安装即可
第三步 开动你的main方法,同时在VisualVM上会看到你的主线程,之后点击Visual GC页签
以刚才我所说的窗口等于-1时的内存状态为例
我们可以看到整个Old区域完全被填满
当我们更改窗口大小时,我列举了一个窗口大小与最终生成时间的对照图:大家可以参考
窗口大小为10万时内存消耗
窗口大小为10000时
窗口大小为1000时
可以看出随着窗口大小减小,old区域的数据量几乎已经不存在
所以可以断定,数据都没有机会晋升到老年代就已经被刷入磁盘了。
年轻带采用复制算法 :
将内存分成一块儿Eden和两块儿Survivor区域。空间比例为8:1:1。当survivor空间不足,需要通过类似银行贷款似的分配担保机制借老年代空间并按时偿还。这种做法简单,高效,大大缩小了内存的空间
当excel对象被刷进磁盘后,相当于这些对象都成了内存中的垃圾,如果窗口增大,内存中数据增多,old区将会因为young区没有及时刷入磁盘的数据而逐渐填满。这也是内存溢出的最主要原因。