欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

多线程写同一个excel文件(导出) 博客分类: Java EE 多线程excel导出文件

程序员文章站 2024-03-12 10:35:20
...

今天是2018.03.22,已经很久没有更新博客了。。这段时间一直挺忙的,也收获很多。最近一个excel导出的任务让我搞了好久,想想踩过的坑,就想上来小结一番。

------------------------------------------------------分割线------------------------------------------------------

 

前沿:对于Java党而言,poi是目前较为流行的对Microsoft Office格式档案读和写的功能的工具。但有时候单线程写一个数据量需求较大或较为耗时(如向excel写图片)的文件时,
如导出,往往太慢而超时。本文将以多线程导出一个excel文件为例(同一个sheet)来提高效率。

 

1.线程与任务
线程池:可以用线程池来管理线程,包括任务的分配和线程的回收。
任务:每个线程待处理的资源,在submit任务时,需保证每个任务不会互相重复。
线程:任务的执行者
(注:任务和线程的概念一定要区分开来!每个线程可以在执行完一次任务后,继续执行还未被处理的任务。线程池submit一个任务不代表会有一个线程立即去执行它)
如何保证所有任务都被执行完,才能够进行IO流的写入操作?
保证所有任务都执行完毕,可以用一个同步工具类:CountDownLatch来记录,初始化它的时候,size是任务数,每执行一次,就countDown。在所有任务执行完毕的代码逻辑后await,
即可防止还有任务没被线程执行完就往下执行,起到阻塞和等待的作用。

 

2.思路流程
首先,一个excel对象在poi里被定义为Workbook, 一个Workbook包含多个Sheet. 每个Sheet有Row, 每行又有Cell(单元格).
先把要往Sheet里写的所有数据按行区分,即先准备好要填充的数据集合(元素是每行的对象,代码略);
建立线程池,管理线程和分配任务:

ExecutorService es = Executors.newFixedThreadPool(40);
//线程执行任务的计数器,初始大小为待填充数据集的size
        CountDownLatch latch = new CountDownLatch(results.size());
        for(int i=0; i<results.size(); i++) {
          ExcelResultVo excelResultVo = results.get(i);
//          RowObj rowObj = groups.get(i);
          es.submit(new Runnable() {
            @Override
            public void run() {
              PoiWrite.writeData(excelResultVo, wb, sheet, patriarch, styleContent);
              latch.countDown();//每个任务执行完就countDown一次
            }
          });

        }
        latch.await();//阻塞,直到计数器的值为0,才让主线程往下执行
        es.shutdown();//关闭线程池

 因为操作的是同一个sheet,而sheet在addRow的时候底层是这样做的:

public void insertRow(RowRecord row) {
		// Integer integer = Integer.valueOf(row.getRowNumber());
		_rowRecords.put(Integer.valueOf(row.getRowNumber()), row);
		// Clear the cached values
		_rowRecordValues = null; 
		if ((row.getRowNumber() < _firstrow) || (_firstrow == -1)) {
			_firstrow = row.getRowNumber();
		}
		if ((row.getRowNumber() > _lastrow) || (_lastrow == -1)) {
			_lastrow = row.getRowNumber();
		}
	}

 /_rowRecords是一个Map<Integer, RowRecord>,而Map是线程非安全的,所以需要在addRow的时候加锁:

public static synchronized HSSFRow getRow(HSSFSheet sheet, int rownum) {
      return sheet.createRow(rownum);

  }

 另:往excel写图片需要图片的byte数组(图片的url地址转byte数组也比较耗时,可以用多线程先准转好,作为填充数据的一个属性),这里其实也是先createRow,再往指定单元格写图片了,大致语法:

HSSFClientAnchor anchor = new HSSFClientAnchor(0, 0,0 , 0, (short) col1, row1, (short) col2, row2);//这里其实createRow了
    HSSFPicture pict = patriarch.createPicture(anchor, wb.addPicture(imgData, HSSFWorkbook.PICTURE_TYPE_JPEG));

    pict.resize(1.0,1.0);

 所以写图片的方法也需要同步,不然会报错。。(血的教训)

经测试:单线程导出1000条数据(含1100张图片)需超过5min的时间,很容易超时。而多线程仅需20s以内!

注:一定要注意多线程操作同一对象的线程安全问题,如多线程操作同一List,一定要用线程安全的List如CopyOnWriteArrayList或用Collections.synchronizedList包装原始的ArrayList。
add的时候才能避免线程安全问题。

 

思考:之前一直对多线程停留在概念的理解上,实践的还较少,现在对于线程安全有更为直观的理解:是否不同线程执行了不同的资源(任务)而没有发生“多个线程重复执行同一资源”
的情况?比如本文的例子,是先把同一个sheet待填充的数据按Row划分,每个任务指定了Row的位置,每个线程负责将每行数据填充到指定位置而不会发生重复填充某一行的情况。这样多个线程执行
一个“大任务”划分出来的每个“小任务”,就能达到节省时间的目的。