poi多线程操作问题(OOM与操作createCell报错ConcurrentModificationException)

前言

在公司项目“北森开放平台”API的对接上,需要将8w多条数据导出到Excel中,

原先单线程访问5000条数据需要20多分钟;

后面利用线程池开启多线程访问API接口来获取信息,6分钟搞定,大概效率提高了5倍,但是!!!

  • 同时导出8w多数据,发现在导出5w多的时候OOM了,原因:数据量太大,造成sheet对象过大,堆空间直接OOM;

    解决方案:

    • 减少不必要的字段
    • 多文件打包(每5000个数据导出做一个文件)
    • 多sheet(先生成excel 写了一个sheet后,重新再去写这个excel,也就是写到5000数据时,写入excel,然后释放内存,再去写这个excel)
  • 发现部分线程会在createCell时报错“ConcurrentModificationException”

    原因:

    cell底层的TreeMap结构,即每行的cells是以TreeMap存储的

    在createCell时

    public XSSFCell createCell(int columnIndex, CellType type) {
        Integer colI = new Integer(columnIndex);
        XSSFCell prev = (XSSFCell)this._cells.get(colI);
        CTCell ctCell;
        if (prev != null) {
            ctCell = prev.getCTCell();
            ctCell.set(Factory.newInstance());
        } else {
            ctCell = this._row.addNewC();
        }
    
        XSSFCell xcell = new XSSFCell(this, ctCell);
        xcell.setCellNum(columnIndex);
        if (type != CellType.BLANK) {
            xcell.setCellType(type);
        }
    
        this._cells.put(colI, xcell);
        return xcell;
    }
    

    先get在put,多线程在Treemap的操作中会出现 遍历修改的问题,即(fail-fast),此时会抛出"java.util.ConcurrentModificationException",所以,将createCell封装为了同步方法,此时可以解决问题,但是性能就提升不多,平均下来17分钟才搞得定。

    ConcurrentModificationException详解

package com.hpsyche;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Hpsyche
 */
public class ListTest {
    public static void main(String[] args) {
        List<String> list=new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");

        for(String s:list){
            if(s.equals("2")){
                list.remove(2);
            }
        }
    }
}

上述代码在删除s为“2”的元素时,报java.util.ConcurrentModificationException异常

但是测试发现改为s.equals(“4”)时是特殊情况,此时并不会报错?

其实,在list的foreach操作中:

@Override
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

在for循环中一开始也是对expectedModCount采用modCount进行赋值。在进行for循环时每次都会有判定条件modCount == expectedModCount,当执行完arrayList.remove(integer)之后:

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

modCount++;再次迭代时,执行if语句,结果抛出java.util.ConcurrentModificationException异常。

  • 1.modCount 时List从new开始,被修改的次数。当List调用Remove等方法时,modCount++
  • 2.expectedModCount是指Iterator现在期望这个list被修改的次数是多少次。是在Iterator初始化的时候将modCount 的值赋给了expectedModCount

那么就解释了为什么会报上述异常:

  • 1.modCount 会随着调用List.remove方法而自动增减,而expectedModCount则不会变化,就导致modCount != expectedModCount。
  • 2.在删除倒数第二个元素后,cursor=size-1,此时size=size-1,导致hasNext方法认为遍历结束。

为什么要这样做呢?

Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 
Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变。
当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。
所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法 remove() 来删除对象, Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性

---------------------------------

比如你在迭代一个ArrayList,迭代器的工作方式是依次返回给你第0个元素,第1个元素,等等,假设当你迭代到第5个元素的时候,你突然在ArrayList的头部插入了一个元素,使得你所有的元素都往后移动,于是你当前访问的第5个元素就会被重复访问。 
java认为在迭代过程中,容器应当保持不变。因此,java容器中通常保留了一个域称为modCount,每次你对容器修改,这个值就会加1。当你调用iterator方法时,返回的迭代器会记住当前的modCount,随后迭代过程中会检查这个值,一旦发现这个值发生变化,就说明你对容器做了修改,就会抛异常。 

解决方案

使用iterator中的remove方法,而不是ArrayList的remove方法

iterator中remove源码如下:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

在remove后会有expectedModCount = modCount;操作,协调两者数值一致。

所以,我们可以把代码改为:

package com.hpsyche;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * @author Hpsyche
 */
public class ListTest {
    public static void main(String[] args) {
        List<String> list=new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        Iterator<String> iterator = list.iterator();
        while(iterator.hasNext()){
            String next = iterator.next();
            if(next.equals("2")){
                iterator.remove();

            }
        }
        for(String s:list){
            System.out.println(s);
        }
    }
}
发布了63 篇原创文章 · 获赞 29 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Hpsyche/article/details/98471478
今日推荐