【Java】StringBuilderとStringBufferの違いをスレッドセーフの観点で検証してみた
Javaで文字列連結をする場合に「String同士の足し算は効率が悪いのでやめましょう!」というよくある話とともに名前が挙がるStringBufferクラスとStringBuilderクラス。
最近だと「StringBufferクラスよりStringBuilderクラスの方が効率的!」というのも同じくらいよく聞くようになったが、そのたびに「ただしStringBuilderはスレッドセーフでないのでマルチスレッドでは使わないこと」と但し書きのような文言が添えられる。
この注意書きは果たして何を意味しているのだろうか、具体的にどういう場合に問題が起こるのかを調べてみた。StringBufferとStringBuilderの連結のパフォーマンス比較の記事はたくさんあるが、マルチスレッドでの使い分けおよび実行結果の違いについてはあまり見つけられなかったので、今回簡単なマルチスレッドなコードでStringBuilderだとまずい場合があることを検証してみた。
StringBufferとStringBuilderの違い
色々なところで言及されているが、要するに、StringBufferはマルチスレッドで同じインスタンスを触ってもOKなように作られているが、StringBuilderはそうなっていない。マルチスレッドで同じStringBuilderインスタンスにアクセスするようなコードは動作
や実行結果が保証できないということを意味している。
StringBufferとStringBuilderの違いを検証
つまり、マルチスレッドでStringBufferの一つのインスタンスにアクセスするコードとStringBuilderの一つのインスタンスにアクセスするコードの動作の違いを見てみればよい。
ということで、以下のコードを動かしてみる。
まず、StringBufferの単一インスタンスにアクセスするRunnableインタフェースを実装したStringBufferTesterクラス。
StringBufferTester.java
public class StringBufferTester implements Runnable { // スレッドセーフなクラス private StringBuffer sb = new StringBuffer(); @Override public void run() { for (int i=0; i<100; i++) { sb.append('#'); } } public int length() { return sb.length(); } }
続いて、StringBuilderの単一インスタンスにアクセスするクラス。上とsbインスタンス変数のクラス以外はまったく同じ実装のクラス。
StringBuilderTester.java
public class StringBuilderTester implements Runnable { // スレッドセーフでないクラス private StringBuilder sb = new StringBuilder(); @Override public void run() { for (int i=0; i<100; i++) { sb.append('#'); } } public int length() { return sb.length(); } }
これをマルチスレッドで実行する。まずはStringBufferの方を試す。スレッド数100で100文字連結するスクリプトで最後に文字数をカウントするので、100×100で「Result:10000」と出力されることが期待される。
ThreadExecuter.java
public class ThreadExecuter { private static final int THREAD_MAX = 100; public static void main(String[] args) throws InterruptedException { StringBufferTester tester = new StringBufferTester(); Thread[] ts = new Thread[THREAD_MAX]; // スレッド初期化 for (int i=0; i<THREAD_MAX; i++) { ts[i] = new Thread(tester); } // スレッド開始 for (Thread t : ts) { t.start(); } // スレッド同期 for (Thread t : ts) { t.join(); } System.out.println("Result:" + tester.length()); } }
これは予想した通りの以下の結果となった。20回ほど実行してみたが、常に同じだった。
StringBufferのとき
Result:10000
さて、次にStringBuilderの方でやってみる。ThreadExecuterクラスを修正し、StringBuilderTesterクラスに切り替えて実行する。
StringBuilderTester tester = new StringBuilderTester();
こちらはときどき以下のように、結果が不正となる場合があった。当環境では10回中2,3回ほどは誤った結果となり、それ以外は10000と正しい結果となった。
StringBuilderのとき
Result:9998
このことから、StringBuilderを使ったマルチスレッドの処理は、同じタイミングでオブジェクトにアクセスするときにロックされないため、あるスレッドでappendされた結果が別スレッドの処理で上書きされ、なかったことにされる場合があるよう。その結果、10000回appendされるはずの処理が9998回になってしまったりしている。
実装の違い
StringBuilderとStringBufferの実装の違いを見てみる。どちらもAbstractStringBuilderクラスを継承したクラスとなっている。
- AbstractStringBuilderクラス : 実際の実装を持つ親クラス
- StringBuilderクラス : スレッドセーフでないサブクラス
- StringBufferクラス : スレッドセーフなサブクラス
appendメソッドは以下のような実装となっている。
StringBuilderのappendメソッド
public StringBuilder append(char c) { super.append(c); return this; }
StringBufferのappendメソッド
public synchronized StringBuffer append(char c) { super.append(c); return this; }
どちらも親クラスのappendを呼び出して自身を返すという全く同じロジックとなっており、違いはsynchronizedがStringBuffer側にはあることのみ。StringBufferはappendメソッドがスレッドセーフな仕様となっている。
結局ここの違いで、StringBufferのappendは同期のためにロック等が発生しているため、その分StringBuilderに比べてパフォーマンスが悪くなっている。
まとめ
StringBuilderはマルチスレッド環境では使ってはいけない!スレッドセーフ性が保証されているところではパフォーマンスに優れたStringBuilderを使い、保証されないところではStringBufferを使おう!
関連記事