最近在項目開發時遇到一個問題,就是寫好的代碼時不時的報出ArrayIndexOutOfBoundsException的異常,這讓我很困擾。下面是那段代碼的簡化版,只是爲了說明這個問題。
1、 代碼及報錯信息
代碼如下:
import java.util.ArrayList;
import java.util.List;
/**
* 測試StringBuilder和StringBuffer在多線程情形下的不同表現
* 說明: StringBuilder是線程不安全的,但是效率較高
* StringBuffer是線程安全的,但是效率較低
* @author Herry
*/
public class StringContactTest {
public static void main(String[] args) {
// 測試StringBuilder拼接方式
for(int i=0; i < 20; i++) {
stringContactWithBuilder();
}
}
// 通過StringBuilder來進行字符串拼接
public static void stringContactWithBuilder(){
// 待拼接數據
List<String> dataList = new ArrayList<>();
// 模擬賦值
for (int i = 0; i < 20; i++) {
dataList.add("data" + i);
}
StringBuilder stringBuilder = new StringBuilder();
dataList.parallelStream().forEach(data -> {
stringBuilder.append(data);
StringBuilder stringBuilder2 = new StringBuilder();
dataList.parallelStream().forEach(data2 -> {
stringBuilder2.append(data2);
});
System.out.println(stringBuilder2.toString());
});
System.out.println(stringBuilder.toString());
}
}
報錯信息如下:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at java.util.concurrent.ForkJoinTask.getThrowableException(Unknown Source)
at java.util.concurrent.ForkJoinTask.reportException(Unknown Source)
at java.util.concurrent.ForkJoinTask.invoke(Unknown Source)
at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(Unknown Source)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(Unknown Source)
at java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.util.stream.ReferencePipeline.forEach(Unknown Source)
at java.util.stream.ReferencePipeline$Head.forEach(Unknown Source)
at com.liu.date20170625.StringContactTest.stringContactWithBuilder(StringContactTest.java:32)
at com.liu.date20170625.StringContactTest.main(StringContactTest.java:17)
Caused by: java.lang.ArrayIndexOutOfBoundsException
at java.lang.System.arraycopy(Native Method)
at java.lang.String.getChars(Unknown Source)
at java.lang.AbstractStringBuilder.append(Unknown Source)
at java.lang.StringBuilder.append(Unknown Source)
at com.liu.date20170625.StringContactTest.lambda$2(StringContactTest.java:36)
at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(Unknown Source)
at java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.util.stream.ForEachOps$ForEachTask.compute(Unknown Source)
at java.util.concurrent.CountedCompleter.exec(Unknown Source)
at java.util.concurrent.ForkJoinTask.doExec(Unknown Source)
at java.util.concurrent.ForkJoinPool.helpComplete(Unknown Source)
at java.util.concurrent.ForkJoinPool.awaitJoin(Unknown Source)
at java.util.concurrent.ForkJoinTask.doInvoke(Unknown Source)
at java.util.concurrent.ForkJoinTask.invoke(Unknown Source)
at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(Unknown Source)
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(Unknown Source)
at java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.util.stream.ReferencePipeline.forEach(Unknown Source)
at java.util.stream.ReferencePipeline$Head.forEach(Unknown Source)
at com.liu.date20170625.StringContactTest.lambda$0(StringContactTest.java:35)
at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(Unknown Source)
at java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.util.stream.ForEachOps$ForEachTask.compute(Unknown Source)
at java.util.concurrent.CountedCompleter.exec(Unknown Source)
at java.util.concurrent.ForkJoinTask.doExec(Unknown Source)
at java.util.concurrent.ForkJoinPool$WorkQueue.execLocalTasks(Unknown Source)
at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(Unknown Source)
at java.util.concurrent.ForkJoinPool.runWorker(Unknown Source)
at java.util.concurrent.ForkJoinWorkerThread.run(Unknown Source)
2、 分析
一開始不知道這個異常是從哪裏拋出來的,後來查看源碼後發現是從append()方法中拋出的,下面是分析過程。
根據報錯信息的提示,異常信息出現在36行,下面進行源碼跟蹤。進入StringBuilder的append()方法,具體代碼如下:
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder的append()方法是調用父類AbstractStringBuilder的append()方法實現的。下面看看AbstractStringBuilder的append()方法的具體實現:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
可以看到,在AbstractStringBuilder的append()方法中,在進行字符串拼接之前會先使用ensureCapacityInternal()檢查空間是否足夠,如果不夠將進行擴容後再進行字符串的拼接。但是從ensureCapacityInternal()方法的註釋中(註釋具體如下)可以看到明確的說明,該方法是不同步的,也就是在多線程情況下是不安全的。
/**
* For positive values of {@code minimumCapacity}, this method
* behaves like {@code ensureCapacity}, however it is never
* synchronized.
* If {@code minimumCapacity} is non positive due to numeric
* overflow, this method throws {@code OutOfMemoryError}.
*/
在空間檢查通過後,調用getChars()方法進行字符串拼接。getChar()具體實現如下:
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
繼續跟蹤下去,查看arraycopy()的源碼,具體如下:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
這是一個本地(native)方法。直到這裏,我們還是沒有找到拋出ArrayIndexOutOfBoundsException異常的地方,但是唯一可能拋出這個異常的也只有arraycopy()了。由於這是本地方法,無法直接在jdk裏找到源碼,於是在網上找了這個方法的源碼,具體如下:
/*
java.lang.System中的arraycopy方法
*/
JVM_ENTRY(void, JVM_ArrayCopy(JNIEnv *env, jclass ignored, jobject src, jint src_pos,
jobject dst, jint dst_pos, jint length))
JVMWrapper("JVM_ArrayCopy");
// Check if we have null pointers
//檢查源數組和目的數組不爲空
if (src == NULL || dst == NULL) {
THROW(vmSymbols::java_lang_NullPointerException());
}
arrayOop s = arrayOop(JNIHandles::resolve_non_null(src));
arrayOop d = arrayOop(JNIHandles::resolve_non_null(dst));
assert(s->is_oop(), "JVM_ArrayCopy: src not an oop");
assert(d->is_oop(), "JVM_ArrayCopy: dst not an oop");
// Do copy
//真正調用複製的方法
s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END
以上方法沒有真正實現複製,而只是簡單的檢測源數組和目的數組不爲空,排除一些異常情況。下面是具體實現的方法:
/*
java.lang.System中的arraycopy方法具體實現
*/
void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
int dst_pos, int length, TRAPS) {
//檢測s是數組
assert(s->is_objArray(), "must be obj array");
//目的數組不是數組對象的話,則拋出ArrayStoreException異常
if (!d->is_objArray()) {
THROW(vmSymbols::java_lang_ArrayStoreException());
}
// Check is all offsets and lengths are non negative
//檢測下標參數非負
if (src_pos < 0 || dst_pos < 0 || length < 0) {
THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException());
}
// Check if the ranges are valid
//檢測下標參數是否越界
if ( (((unsigned int) length + (unsigned int) src_pos) > (unsigned int) s->length())
|| (((unsigned int) length + (unsigned int) dst_pos) > (unsigned int) d->length()) ) {
THROW(vmSymbols::java_lang_ArrayIndexOutOfBoundsException());
}
// Special case. Boundary cases must be checked first
// This allows the following call: copy_array(s, s.length(), d.length(), 0).
// This is correct, since the position is supposed to be an 'in between point', i.e., s.length(),
// points to the right of the last element.
//length==0則不需要複製
if (length==0) {
return;
}
//UseCompressedOops只是用來區分narrowOop和oop,具體2者有啥區別需要再研究
//調用do_copy函數來複制
if (UseCompressedOops) {
narrowOop* const src = objArrayOop(s)->obj_at_addr<narrowOop>(src_pos);
narrowOop* const dst = objArrayOop(d)->obj_at_addr<narrowOop>(dst_pos);
do_copy<narrowOop>(s, src, d, dst, length, CHECK);
} else {
oop* const src = objArrayOop(s)->obj_at_addr<oop>(src_pos);
oop* const dst = objArrayOop(d)->obj_at_addr<oop>(dst_pos);
do_copy<oop> (s, src, d, dst, length, CHECK);
}
}
這裏只是爲了找到異常拋出的地方和原因,所以不再繼續往下分析,比如do_copy()方法的具體實現等沒有深究。綜合前面的ensureCapacityInternal()方法的非同步,到這裏我們可以發現異常拋出的原因了:因爲append()方法是在多線程(parallelStream,並行流)中調用的,所以可能有兩個或者多個線程通過了ensureCapacityInternal()方法的空間校驗,而實際空間不足而導致了數組下標越界。下面舉個簡單的例子進行理解。
假如有A、B兩個線程,都需要拼接一個長度爲40的字符串,而當前剩餘空間爲50。當A通過ensureCapacityInternal()檢驗且爲執行getChars()方法時被掛起,這時B線程通過ensureCapacityInternal()對空間進行校驗是可以通過的,因爲40<50。接下來當A、B線程進行數組複製時,後複製的那個線程將出現數組下標越界異常,因爲第一個線程複製完成後,剩下空間只有10。10<40而導致空間不足,下標越界。
3、 結語
所以針對開頭的那個示例代碼,有兩種改法,一種是將並行流(parallelStream)改成串行流(stream),二是將非線程安全的StingBuilder更換成線程安全的StringBuffer。
以前對於多線程程序寫得比較少,遇到這個問題時,有些無從下手,所以決心深究一下其原因。雖然平時也知道StringBuilder是線程不安全、StringBuffer是線程安全的,但是用起來有時候確實不太注意。因此將問題及解決方法記錄下來,希望以後可以引以爲戒,也希望對你有些許幫助,那就足矣。如有哪裏寫得不對,還望大神不吝賜教,不勝感激。
【參考博客】 http://blog.csdn.net/u011642663/article/details/49512643