Go to Contents Go to Java Page
J2SE 1.5 虎の穴
 
 

文字列操作 スレッドセーフ vs パフォーマンス StringBuilder

 
 
Tiger StringBuffer vs StringBuilder
 
 

StringBuilder クラスは Tiger で導入されたクラスなのですが、なんと機能は StringBuffer クラスとまったく同じなのです。

それじゃ、なぜ今になって新しいクラスを作ったのでしょうか。

それはスレッドセーフとパフォーマンスに関連します。

StringBuffer クラスのすべてのメソッドは synchronized になっており、同期化されています。このため、複数のスレッドから使用される場合でも、安全に使用することができます。

その一方で同期化にはコストがかかります。簡単にいえばメソッドを synchronized にすると遅くなってしまうのです。

しかし、StringBuffer を使うときに本当にスレッドセーフが必要ですか?

私が書いてきたコードには StringBuffer クラスにスレッドセーフが必要だったケースはほとんどありませんでした。それなのにわざわざ同期化した StringBuffer クラスを 使う必要はありません。

そこで、登場したのがスレッドセーフではない StringBuilder クラスです。

StringBuffer クラスと StringBuilder クラスの違いは、ちょうど java.util.Vector クラスと java.util.ArrayList クラスの違いと同じようなものです。

機能は同じなので、スレッドセーフが必要かどうかで選べばいいと思います。

 

 
 
Tiger 比較してみよう
 
 

同期化されていない分 StringBuilder クラスのパフォーマンスは StringBuffer クラスより高いはずです。そこで、簡単なアプリケーションで試してみました。

サンプルのソース StringBuilderTest.java

単に append メソッドを何度も呼んでいるだけです。

public class StringBuilderTest {
    public StringBuilderTest() {
        for (int i = 0; i < 10; i++) {
            append1();
            append2();
        }
    }
 
    private void append1() {
        StringBuffer buffer = new StringBuffer();
         
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            append(buffer);
        }
        long end = System.currentTimeMillis();
  
        System.out.println("StringBuffer: " + (end - start));
    }
 
    private void append(StringBuffer buffer) {
        for (char c = (char)0; c < (char)10000; c++) {
            buffer.append(c);
        }
    }
 
    private void append2() {
        StringBuilder builder = new StringBuilder();
          
        long start = System.currentTimeMillis();
        for (int i = 0; i < 1000; i++) {
            append(builder);
        }
        long end = System.currentTimeMillis();
 
        System.out.println("StringBuilder: " + (end - start));
    }
 
    private void append(StringBuilder builder) {
        for (char c = (char)0; c < (char)10000; c++) {
            builder.append(c);
        }
    }
 
    public static void main(String[] args) {
        new StringBuilderTest();
    }
}

これを PentiumM 1.7MHz メモリ 512MB の Windows XP のマシンで実行してみました。

C:\examples>java StringBuilderTest
StringBuffer: 813
StringBuilder: 453
StringBuffer: 531
StringBuilder: 406
StringBuffer: 547
StringBuilder: 406
StringBuffer: 547
StringBuilder: 391
StringBuffer: 547
StringBuilder: 406
StringBuffer: 531
StringBuilder: 391
StringBuffer: 532
StringBuilder: 406
StringBuffer: 578
StringBuilder: 422
StringBuffer: 547
StringBuilder: 390
StringBuffer: 547
StringBuilder: 422
   
C:\examples>

はじめの 813 ms は省いたとしても、だいたい 2 割から 3 割りぐらい高速になっているようです。

現状の HotSpot の実装では synchronized のコストはずいぶん減っているので、こんなものなのでしょう。私はもっと差が少ないかと思っていたぐらいです。

 

 
 
Tiger こんなに風にも使われている
 
 

文字列の連結には StringBuffer クラスを使うようにとよくいわれますが、Tiger では javac がうまく変換してくれます。たとえば、次に示すソースだとどうなるでしょうか。

public class StringAppendTest {
    public static void main(String[] args) {
        System.out.println("現在時刻 " + new java.util.Date());
    }
}

これをコンパイルし、Jad で逆コンパイルするとこうなりました。見やすくするため少しだけ編集してあります。

import java.io.PrintStream;
import java.util.Date;
 
public class StringAppendTest
{
 
    public StringAppendTest()
    {
    }
 
    public static void main(String args[])
    {
        System.out.println((new StringBuilder()).
                           append("\u73FE\u5728\u6642\u523B ").
                           append(new Date()).toString());
    }
}

javac が StringBuilder を使って最適化をしてくれます。こんな風にも使われているのですね。

 

 
 
Tiger StringBuffer も変わった ?
 
 

StringBuilder クラスと StringBuffer クラスのソースを見比べると面白いことがわかります。

J2SE 1.4 までは StringBuffer クラスは通常のクラスでしたが、Tiger では AbstractStringBuilder クラスの派生クラスになりました。

StringBuffer と StringBuilder のクラス図

もちろん、AbstractStringBuilder クラスは Tiger で導入されたものです。また、Appendable インタフェースも Tiger で導入されました。

たとえば、StringBuffer#append(String str) メソッドと StringBuilder#append(String str) メソッドを比較してみましょう。

    public synchronized StringBuffer append(String str) {
        super.append(str);
        return this;
    }

StringBuffer#append(String str)

    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

StringBuffer#append(String str)

違いは synchronized であるかどうかだけで、実際の処理は AbstractStringBuilder が行っていることがわかります。AbstractStringBuilder クラスの append(String str) メソッドも見てみましょう。

    public AbstractStringBuilder append(String str) {
        if (str == null) str = "null";
        int len = str.length();
        if (len == 0) return this;
        int newCount = count + len;
        if (newCount > value.length)
            expandCapacity(newCount);
        str.getChars(0, len, value, count);
        count = newCount;
        return this;
    }

参考までに J2SE 1.4.2 の StringBuffer#append(String str) メソッドは次のように定義されています。

    public synchronized StringBuffer append(String str) {
        if (str == null) {
            str = String.valueOf(str);
        }

        int len = str.length();
        int newcount = count + len;
        if (newcount > value.length)
            expandCapacity(newcount);
        str.getChars(0, len, value, count);
        count = newcount;
        return this;
    }

null の時の処理など少しだけ違いますが、基本的には同じ処理をしています。

つまり、J2SE 1.4 までの StringBuffer クラスの処理を Tiger では AbstractStringBuilder クラスにうつしてしまったということです。

そして、StringBuffer クラスと StringBuilder クラスは処理を AbstractStringBuilder クラスに委譲してしまっています。この 2 つのクラスの違いは単に synchronized がついているかいないかというという違いだけになっているわけです。

ここで使用されたリファクタリングなど、Java のソースコードはお手本にしたいテクニックが満載されていますね。

あるバージョンのソースを見るだけでもいいとは思うのですが、今回の例のように複数のバージョンのソースを追いかけることでコードをどのように洗練させていくかを知ることもできるのです。

 

 
 
Tiger おわりに
 
 

単に synchronized がはずされただけですが、StringBuilder クラスは使う場面が多そうです。

複数のスレッドから文字列操作を行うのであれば StringBuffer クラス、単一スレッドであれば StringBuilder クラスという使い分けをおこなうようにすれば OK です。

実際にアプリケーションをプロファイリングすると、文字列操作は頻度が高い場合が多いのではないでしょうか。そんなときに、StringBuffer クラスから StringBuilder クラスに変更するだけでパフォーマンスが向上するわけですから、使わない理由はありません。

今回使用したサンプルはここからダウンロードできます。

 

(Mar. 2004)

 
 
Go to Contents Go to Java Page