前言
在公司项目“北森开放平台”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);
}
}
}