こんにちは。ハンドルネーム「Javaを復習する初心者」です。このサイトはプログラミング言語Javaの復習・学習をするブログです。プログラムの開発・実行はEclipseで行ってます。
スポンサーリンク
お知らせ
  • 参考文献のページ作りました。
  • Amazon.co.jpアソシエイトに参加していますが、参考文献の紹介はもしもアフィリエイトに統一しました。
  • 2016年10月9日からは投稿ペースを落とします。週1回くらいにする予定です。
スポンサーリンク

スレッドでsynchronizedを使う

こんにちは。「Javaを復習する初心者」です。今回は複数のスレッドから同じオブジェクトを操作することをしました。synchronizedを使うことで処理を同期させるということをやりました。

StringBuilderクラスという可変の文字列を扱うクラスがあります。append()メソッドで文字列を追加することができます。このクラスなんですが、API仕様書には「同期化は保証されません」という記載があります。そこで、このクラスのインスタンスに対し、2つのスレッドからappend()メソッドを呼び出す実験をしました。

テスト用メソッド

以下のようにテスト用メソッドを作成しました。

    private static int test1() {
        StringBuilder sb = new StringBuilder();

        Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    sb.append("a");
                }
            });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                sb.append("a");
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return sb.length();
    }

上記でThread#startメソッドはスレッドを開始するメソッドです。Thread#joinメソッドはスレッドの終了するのを待機するメソッドです。待機しない場合、先にtest1メソッドのreturn文が実行されてしまいます。

StringBuilder型変数sbに対して、2つのスレッドt1とt2がappend()メソッドを呼び出してます。それぞれ1000回ずつ呼び出しています。このメソッドの返却値はsbの長さです。StringBuilderは同期化は保証されないため、sbの長さが2000より小さい値になる可能性があります。このことを確認するのが目的です。

出力

以下のように、このテストメソッドを10回呼び出し、返却された値を出力してみました。

        for (int i = 1; i <= 10; i++) {
            System.out.printf("%2d回目: %3d", i, test1());
            System.out.println();
        }

結果は以下です。

 1回目: 2000
 2回目: 2000
 3回目: 2000
 4回目: 1925
 5回目: 2000
 6回目: 1987
 7回目: 1970
 8回目: 2000
 9回目: 2000
10回目: 2000

上記結果は何回か実行した中での結果の一つです。結果が2000にならない場合があります。これは2つのスレッドがほぼ同時にappend()メソッドを呼び出した時があるということです。

上記結果はエラーの出力がありませんが、時々、ArrayIndexOutOfBoundsExceptionが発生する場合がありました。

非同期の場合

さて、上記のように同期していない場合は期待動作と異なる可能性があります。なのでsynchronizedキーワードを使います。

以下、synchronizedを追加した場合です。

    private static int test2() {
        StringBuilder sb = new StringBuilder();

        Thread t1 = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    synchronized (sb) {
                        sb.append("a");
                    }
                }
            });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                synchronized (sb) {
                    sb.append("a");
                }
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return sb.length();
    }

synchronized (sb) {}と記述することで、sbをロックして処理することできます。同時にappend()メソッドが実行されることがなるなるため、実効結果は以下のようになります。

 1回目: 2000
 2回目: 2000
 3回目: 2000
 4回目: 2000
 5回目: 2000
 6回目: 2000
 7回目: 2000
 8回目: 2000
 9回目: 2000
10回目: 2000

何度か試しましたが、必ず2000が出力されてました。