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

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

前回の記事では Java の同期化(synchronized)について説明してきました。

hjm333.hatenablog.com

今回の記事では「volatile」と「同期化が必要ない場合」について説明します。

volatile

Java の同期化(synchronized)は次の性質を保証するものでした。

  • アトミック性
  • メモリ可視性

Java にはメモリの可視性だけを保証するための仕組みがあります。 それが volatile です。

volatile キーワードが付けられた変数はメモリの可視性が保証されます。 volatile では処理をアトミックにすることはできませんが、各スレッド間でフラグを共有する際に便利です。

例えば、前回、例に挙げたスレッドを停止させるコードでは synchronized を使いました。

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();
    }
}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();
    }
}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();
    }
}

しかし、この例では処理のアトミック性は必要ありません。 boolean の読み書き自体がアトミックになっていますので単にメモリの可視性を保証すれば問題ありません。 そのため volatile を使うことができます。

volatile を使って書き換えたのが次のコードです。

public class StopThread {
    private static volatile boolean stop;

    public static void main(String[] args) throws InterruptedException {
        Thread background = new Thread(new Runnable() {
            @Override public void run() {
                int i = 0;
                while (stop) {
                    i++;
                }
            }
        });
        background.start();

        TimeUnit.SECONDS.sleep(1);
        stop = true;
    }
}

このコードは synchroznied を使ったときと同じように正しく動作します。

インクリメントとデクリメント

volatile はメモリの可視性を保証しますが、アトミック性は保証しません。 そのため「アトミックのように見えて実はアトミックではない」という操作に対して volatile を使わないように注意が必要です。

インクリメントとデクリメントは「アトミックに見えるが実はアトミックでない操作」の代表です。 インクリメントは実際には「値の読み出し」、「値の加算」、「値の書き込み」という複数の操作で構成される処理です。 インクリメント・デクリメントに関わる操作を volatile で実装するのは間違いです。

次のコードは複数のスレッドから一意の番号を生成するためのメソッドを実装したものです。

private static volatile int serialNumber = 0;

public static int generateSerialNumber() {
    return serialNumber++;
}

serialNumber のインクリメントは volatile によってメモリの可視性が保証されています。 ただし、アトミックでないため複数のスレッドから呼び出された際に、それぞれのスレッドで同じ値を生成してしまう可能性があります。

正しくは次のように syncronized を使い、メモリの可視性に加えてアトミック性も保証する必要があります。

private static int serialNumber = 0;

public static synchronized int generateSerialNumber() {
    return serialNumber++;
}

同期を回避する

ここまで同期について説明してきましたが、正しく同期を実装すると複雑になるケースが多いです。 一方、同期がなければコードの複雑度は下がります。

ここでは同期を回避するためのテクニックを紹介します。

不変クラスの利用

不変クラスを利用すれば同期は必要はありません。 不変クラスはインスタンスが生成されてから状態が変わることがないからです。

状態を変える処理が存在しないため、アトミック性について気にすることはありません。 加えて、状態が変わらないので常にキャッシュの値を参照したとしても問題ありません。 すなわち、メモリの可視性について考えなくてよいのです。

可変データをスレッドに閉じ込める

たとえ可変なデータであっても、単一のスレッドからしかアクセスできないようにすれば同期は不要です。

単一スレッドからしか読み書きしませんので、アトミック性もメモリの可視性も考える必要はありません。

スレッドセーフクラス

スレッドセーフ性が保証されているクラスを使えば同期は必要ありません。

同期はそのクラス自体が適切に行ってくれます。 また、同期を使わずにスレッドセーフ性を実現するクラスもあります。

たとえばさきほどの serialNumber の例では java.concurrent の AtomicLong というクラスが利用できます。

private static final AtomicLong serialNumber = new AtomicLong();

public static long generateSerialNumber() {
    // getAndIncrement メソッドは値の読取りとインクリメントをスレッドセーフで行うメソッド
    return serialNumber.getAndIncrement();
}

java.concurrent ライブラリには高度に最適化されたスレッドセーフなクラスが多数あります。 自分でスレッドセーフを実装する前にこれらのライブラリの利用を検討するべきです。

その他

その他のテクニックとして次のような手法がありますが、ここでは説明を省きます。

  • オブジェクトを事実上不変にする
  • オブジェクトを安全に公開する

これらのテクニックに関しては次の書籍に詳しく記載されています。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

感想

ついに concurrency の章か。。。

concurrency についてはもう一度『Java 並行処理プログラミング』を読み直そうかと思ったり、思わなかったり。。。