StringBuilderがスレッドセーフではないのはなぜですか?
Why Is Stringbuilder Not Thread Safe
著者:銭山
Juejin.im/post/5d6228046fb9a06add4e37fe
前書き
インタビュアー: StringBuilderとStringBufferの違いは何ですか?
私:StringBuilderはスレッドセーフではありません、StringBufferはスレッドセーフです
インタビュアー: StringBuilderの不安はどこにありますか?
私:。 。 。 (ダム)
その前は、StringBuilderはスレッドセーフではなく、StringBufferはスレッドセーフであるという結論を思い出しただけでした。 StringBuilderが安全でない理由については、私は考えたことはありませんでした。
分析
この問題を分析する前に、StringBuilderとStringBufferの内部実装が、char配列を介して文字列を格納するStringクラスと同じであることを知っておく必要があります。違いは、Stringクラスのchar配列が最終的に変更され、不変であるということです。 StringBuilderとStringBufferのchar配列は可変です。
まず、コードの一部を見て、マルチスレッドのStringBuilderオブジェクトでどのような問題が発生するかを確認しましょう。
public class StringBuilderDemo { public static void main(String[] args) throws InterruptedException { StringBuilder stringBuilder = new StringBuilder() for (int i = 0 i <10 i++){ new Thread(new Runnable() { @Override public void run() { for (int j = 0 j < 1000 j++){ stringBuilder.append('a') } } }).start() } Thread.sleep(100) System.out.println(stringBuilder.length()) } }
このコードは10個のスレッドを作成し、各スレッドは1000回ループして、StringBuilderオブジェクトに文字を追加していることがわかります。通常の状況では、コードは10000を出力するはずですが、実際の出力はどうなりますか?
予想される10000未満の「9326」が出力され、ArrayIndexOutOfBoundsExceptionもスローされることがわかります(例外は必須ではありません)。
1.出力値が期待値と異なる理由
StringBuilderの2つのメンバー変数を見てみましょう(これらの2つのメンバー変数は実際にはAbstractStringBuilderで定義されており、StringBuilderとStringBufferの両方がAbstractStringBuilderを継承しています)
//The specific content of the storage string char[] value //Number of character arrays already used int count
StringBuilderのappend()メソッドを見てください。
@Override public StringBuilder append(String str) { super.append(str) return this }
StringBuilderの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 }
最初にコードの5行目と6行目を無視しましょう。 7行目を直接見ると、count + = lenはアトミック操作ではありません。このとき、カウント値が10、len値が1であると仮定します。両方のスレッドが7行目を同時に実行します。得られたカウント値は10です。加算演算が実行された後、結果がカウントに割り当てられるため、2つのスレッドが実行された後、カウント値は12ではなく11になります。これがテストコードの出力値が10000。
2.ArrayIndexOutOfBoundsExceptionがスローされる理由。
AbstractStringBuilderのappend()メソッドのソースコードの5行目を振り返ってみましょう。 sureCapacityInternal()メソッドは、StringBuilderオブジェクトの元のchar配列容量が新しい文字列を保持できるかどうかをチェックします。持ちこたえられない場合は、char配列でexpandCapacity()メソッドを呼び出します。拡張。
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity) }
容量拡張のロジックは新しい新しいchar配列であり、新しいchar配列の容量は元のchar配列の2倍に2を加えたものであり、元の配列の内容はSystem.arryCopy( )関数であり、ポインタは最終的に新しいchar配列を指します。
void expandCapacity(int minimumCapacity) { //Calculate new capacity int newCapacity = value.length * 2 + 2 // Some inspection logic is omitted in the middle ... value = Arrays.copyOf(value, newCapacity) }
Arrys.copyOf()メソッド
public static char[] copyOf(char[] original, int newLength) { char[] copy = new char[newLength] // copy array System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)) return copy }
AbstractStringBuilderのappend()メソッドのソースコードの6行目は、Stringオブジェクトのchar配列の内容をStringBuilderオブジェクトのchar配列にコピーすることです。コードは次のとおりです。
str.getChars(0, len, value, count)
getChars()メソッド
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) { ///Some checks are omitted in the middle ... System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin) }
コピープロセスを以下に示します。
StringBuilderのappend()メソッドを同時に実行している2つのスレッドがあり、両方のスレッドが5行目のensureCapacityInternal()メソッドを実行しているとします(現時点ではcount = 5)。
このとき、スレッド1のCPUタイムスライスは使い果たされ、スレッド2は実行を継続します。スレッド2がappend()メソッド全体を実行した後、カウントは6になります。
スレッド1が6行目でstr.getChars()メソッドを実行し続けると、取得されるカウント値は6になり、char配列をコピーするときにArrayIndexOutOfBoundsException例外がスローされます。
この時点で、StringBuilderが安全でない理由が分析されました。テストコードのStringBuilderオブジェクトをStringBufferオブジェクトに置き換えるとどうなりますか?
もちろん出力は10000!
では、StringBufferはスレッドセーフを確保するためにどのような意味を使用しますか? StringBufferのappend()メソッドをクリックすると、この問題を知ることができます。
推奨読書(クリックしてジャンプして読む)
1.1。 SpringBootコンテンツ集約
3.3。 デザインパターンコンテンツの集約
5.5。 マルチスレッドコンテンツの集約