StringBuilderが投げるArrayIndexOutOfBoundsExceptionの探索
それは、書かれたコードが時々ArrayIndexOutOfBoundsExceptionを報告し、私を大いに悩ませたことです。この問題を説明するために、そのコードを簡略化したものを以下に示します。
1. コードとエラーメッセージ
コードは以下の通りです。
import java.util.ArrayList;
import java.util.List;
/**
* Test the different performance of StringBuilder and StringBuffer in multi-threaded situations
* StringBuilder is thread-insecure, but more efficient.
* StringBuffer is thread-safe, but less efficient
* @author Herry
*/
public class StringContactTest {
public static void main(String[] args) {
// Test the StringBuilder splicing method
for(int i=0; i < 20; i++) {
stringContactWithBuilder();
}
}
// String stitching by StringBuilder
public static void stringContactWithBuilder(){
// to be spliced data
List<String> dataList = new ArrayList<>();
// simulate assignment
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)
forkJoinTask.invoke(Unknown Source) at java.util.concurrent.
ForEachOps$ForEachOp.evaluateParallel(Unknown Source) at java.util.stream.
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(Unknown Source)
at java.util.stream.AbstractPipeline.evaluate(Unknown Source)
forEach(Unknown Source) at java.util.stream.ReferencePipeline.
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)
String.getChars(Unknown Source) at java.lang.
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)
forEachRemaining(Unknown Source) at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(Unknown Source)
at java.util.stream.AbstractPipeline.copyInto(Unknown Source)
ForEachOps$ForEachTask.compute(Unknown Source) at java.util.stream.
CountedCompleter.exec(Unknown Source) at java.util.concurrent.
forkJoinTask.doExec(Unknown Source) at java.util.concurrent.
at java.util.concurrent.ForkJoinPool.helpComplete(Unknown Source)
at java.util.concurrent.ForkJoinPool.awaitJoin(Unknown Source)
forkJoinTask.doInvoke(Unknown Source) at java.util.concurrent.
forkJoinTask.invoke(Unknown Source) at java.util.concurrent.
ForEachOps$ForEachOp.evaluateParallel(Unknown Source) at java.util.stream.
at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateParallel(Unknown Source)
at java.util.stream.AbstractPipeline.evaluate(Unknown Source)
forEach(Unknown Source) at java.util.stream.ReferencePipeline.
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)
forEachRemaining(Unknown Source) at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(Unknown Source)
at java.util.stream.AbstractPipeline.copyInto(Unknown Source)
ForEachOps$ForEachTask.compute(Unknown Source) at java.util.stream.
CountedCompleter.exec(Unknown Source) at java.util.concurrent.
forkJoinTask.doExec(Unknown Source) at java.util.concurrent.
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);
これはネイティブメソッドです。ここまでで、ArrayIndexOutOfBoundsExceptionがどこで投げられるのかまだわかりませんでしたが、投げられるのはarraycopy()だけでした。これはローカルメソッドであるため、jdkで直接ソースコードを見つけることができないので、以下のようにネット上でこのメソッドのソースコードを見つけました。
/*
The arraycopy method in java.lang.System
*/
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
// Check that the source and destination arrays are not null
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
// actually call the method that makes the copy
s->klass()->copy_array(s, src_pos, d, dst_pos, length, thread);
JVM_END
上記のメソッドは、実際にはコピーを実装しているわけではなく、単にコピー元とコピー先の配列が空でないことを検出し、いくつかの例外を除外しているだけです。以下は、このメソッドの具体的な実装です。
/*
The concrete implementation of the arraycopy method in java.lang.
*/
void ObjArrayKlass::copy_array(arrayOop s, int src_pos, arrayOop d,
int dst_pos, int length, TRAPS) {
// detect that s is an array
assert(s->is_objArray(), "must be obj array");
//Throw an ArrayStoreException if the destination array is not an array object
if (!d->is_objArray()) {
THROW(vmSymbols::java_lang_ArrayStoreException());
}
// Check is all offsets and lengths are non negative
// 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
// Check if the subscript parameter is out of bounds
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 then no copy is needed
if (length==0) {
return;
}
// UseCompressedOops is only used to distinguish between narrowOop and oop, what is the difference between the two needs to be studied
//call the do_copy function to 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) 環境で呼び出されるため、2つ以上のスレッドが ensureCapacityInternal() メソッドのスペースチェックを通過し、配列添え字が境界外にあるためスペースが足りなくなった可能性があります。
わかりやすい例で説明します。
AとBの2つのスレッドがあり、どちらも長さ40の文字列をステッチする必要があり、現在の残りスペースが50の場合、Aが ensureCapacityInternal() によるチェックと getChars() メソッドの実行中にハングしたとき、スレッドBは ensureCapacityInternal() によってスペースをチェックし 次にスレッドAとBが配列をコピーするとき、最初のスレッドがコピーを終了した後、残りスペースは10だけなので後からコピーするスレッドは配列添字境界外の例外を持つことになります。10<40となり、スペースが足りず、添え字が境界外になってしまいます。
3. 結論
つまり、冒頭のサンプルコードについては、並列ストリーム(parallelStream)をシリアルストリーム(stream)に変更する方法と、スレッドセーフでないStingBuilderをスレッドセーフなStringBufferに置き換える方法の2つがあるわけです。
これまでマルチスレッドのプログラムを書くことが少なかったので、この問題に遭遇したときは少し戸惑いましたが、その原因を深く調べてみようと思いました。普段からStringBuilderがスレッドインセキュア、StringBufferがスレッドセーフであることは知っていても、使うときにはあまり気にしないことがあるのです。そこで、問題と解決策を記録し、今後、警告することができることを期待するだけでなく、いくつかのヘルプを持っていることを期待し、それで十分でしょう。何か問題がある場合、私は神々がアドバイスを与えることを躊躇しないことを願って、私は感謝しています。
[参照元ブログ】をご覧ください。] http://blog.csdn.net/u011642663/article/details/49512643
関連
-
Eclipse問題 アクセス制限。タイプ 'SunJCE' が API でないことを解決し、/jdk ディレクトリにある /jre と jre の違いについて理解を深める。
-
javaの非静的メソッドを静的に参照することができない
-
Springの設定でxsdファイルのバージョン番号を設定しない方が良い理由
-
ajax コミット リソースの読み込みに失敗しました: サーバーはステータス 400 で応答しました ()
-
シェルコマンドやスクリプトのJavaコール
-
-bash: java: コマンドが見つからない 解決方法
-
BindException: アドレスはすでに使用中です:バインドエラー解決
-
eclipse にリソースリーク:'in' が閉じない
-
代入の左辺は変数でなければならない 解答
-
git pull appears現在のブランチに対するトラッキング情報がありません。
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
Solve モジュールのビルドに失敗しました。Error: ENOENT: no such file or directory エラー
-
Jsoup-Crawlingの動作
-
CertificateException: XXXに一致するサブジェクトの代替DNS名が見つかりません 解決策
-
Javaがエラーで実行される、選択が起動できない、最近起動したものがない
-
春ブート複数のデータソースの管理(atomikos)同じサーバーホスト上の複数のプロジェクトを開始する複数のJava - jarのエラーソリューション
-
テストが空であるかどうかを判断するためのオプションの処理
-
Javaジェネリックの深い理解
-
Java上級(XLVI) ArrayList、Vector、LinkedListの類似点と相違点を簡単に説明できる。
-
JSoupは、新バージョンの正方学務システム(イントラネット-学務システム)にログインし、情報処理の詳細をクロールするシミュレーションを行います。
-
Prologでは、コンテンツは許可されていません。