読者です 読者をやめる 読者になる 読者になる

Java の同期化とは何か(1)(【EffectiveJava】項目66:共有された可変データへのアクセスを同期する-前半-)

複数のスレッドで可変データを共有する場合にはそれぞれのスレッドを同期化(synchronized)する必要があります。

Java の同期化は次の二つの性質を保証します。

  • アトミック性(処理の排他制御
  • メモリの可視性

アトミック性

異なるスレッドが共通のデータにアクセスする場合、処理途中の不整合な状態を外部に見せたくない場合があります。 そのためには、処理をアトミックにして同時に一つのスレッドだけが処理することを保証する必要があります。

Java では、処理をアトミックにしたい場合は synchronized キーワードを利用します。 synchronized キーワードを使うとメソッドや処理をアトミックにすることができます。

次の atomic メソッドは常に1つのスレッドでのみ処理されることが保証されます。

public synchronized void atomic() {
    ...(アトミックな処理)...
}

もし、あるスレッド A で atomic() を呼び出していた場合、他のスレッド B で atomic() を呼び出すとスレッド A の atomic() の処理が終わるまでスレッド B は待機します。

変数の読み書き処理はアトミックであることが Java の仕様によって保証されています。 そのため、単純な変数の読み書きに synchronized を使う必要はありません。

// 変数の書きこみはアトミック
int value = 1;

ただし、long と double についてはアトミック性は保証されていないので注意が必要です。

// 次の命令はアトミックではない。二つのバイト命令に変換される可能性がある。
long value = 1L;

メモリの可視性

スレッドが複数ある場合、それぞれのスレッドは独自のキャッシュメモリを持っている可能性があります。

あるスレッド Aで書き込んだ値はスレッド A のキャッシュメモリにのみ反映されていてメインメモリに反映されていません。 この状態では他のスレッド B から値を読み込もうとしても、メインメモリに反映されていないため正しい値を読み込めません。

このように、メモリ上の正しい値が観測できるかどうかの性質を「メモリの可視性」と呼んでいます。

例えば、メインスレッドから他のスレッド A を停止させるコードを考えてみます。 この場合、単に次のようなコードを書くかもしれません。

public class StopThread {
    // boolean は操作がアトミックだから synchronized を利用しない(大きな間違い!)
    private static boolean stop;

    public static void main(String[] args) throws InterruptedException {
        // スレッドの生成
        Thread threadA = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (!stop) {
                    i++;
                }
            }
        });
        // スレッド A の開始
        threadA.start();

        TimeUnit.SECONDS.sleep(1);
        // スレッド A を停止!
        stop = true;
    }
}

実際、このプログラムを動作させると永遠に threadA が終わらないことが判明します。

これはまさにメモリの可視性の問題です。 stop = true; という書込みはメインスレッドで行われていますが、この書込み結果が threadA からは見えていないのです。

threadA が書き込んだ値を見られるようにするには、同期化が必要です。 Java の同期化はアトミック性に加えてメモリの可視性も保証します。

同期化を使って次のようにコードを変更すると、先ほどのプログラムは正しく動作します。 同期化によってメモリの可視性が保証されるからです。

public class StopThread {
    private static boolean stop;
    private static synchronized void requestStop() {
        stop = true;
    }
    private static  synchronized boolean isStop() {
        return stop;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread background = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (isStop()) {
                    i++;
                }
            }
        });
        background.start();

        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

同期化は次の動作を保証します。(『Javaの理論と実践: Javaメモリ・モデルを修正する 第2回』を読んで - The King's Museum)

  • synchronized ブロックに入る時、すべてメインメモリを見るようになる
  • synchronized ブロックを出る時、すべてメインメモリに書き込む

この事実から分かるように、書き込み側にだけ synchronized を指定しても意味がありません。

書き込み側の synchronized によってメインメモリに値が反映されることは保証されます。 一方、読み込み側に synchronized が指定されていないと自分のキャッシュの値を見てしまい、正しくない値を見てしまう可能性があります。

次回

まとめきれなかったので volatile とかは次回に…。