結城先生のデザパタ本マルチスレッド編 第 1 章のまとめ

問題

複数のスレッドがインスタンスを共有している状態で、 それぞれのスレッドがインスタンスの状態を勝手に変更すると、インスタンスの安全性が失われます。

解決方法

そこで、そのままではインスタンスが不定な状態に陥ってしまう範囲を定めます。 これをクリティカルセクションと呼びます。 このクリティカルセクションを synchronized を用いて 1 つのスレッドだけが実行できるようにガードします。 このようなパターンを Single Threaded Execution パターンと呼びます。

関連するパターン

  • インスタンスの状態が変化しないときには Immutable パターンを使うことにより、スループットを向上させることができる
  • インスタンスの状態を参照するスレッドと変更するスレッドが分かれている場合は Read-Write Lock パターンを使うことにより、スループットを向上させることができる

以下のようなカウンターがあったとします

class Counter {

    private Integer count;
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    Counter(final Integer count) {
        this.count = count;
    }

    public void increment() {
        count++;
        logger.info("count: {}", count);
    }

}

これを次のように複数のスレッドから呼び出すとおかしなことが起こります

    private static final Counter counter = new Counter(0);

    public static void main() throws Exception {
        final Runnable runnable = () -> {
            IntStream.range(0, 100).forEach(i -> counter.increment());
        };
        final Thread thread1 = new Thread(runnable);
        final Thread thread2 = new Thread(runnable);
        final Thread thread3 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread3.start();

        thread1.join();
        thread2.join();
        thread3.join();
    }

実行すると同じ値がログ出力されたり、 300 回 increment が呼び出されているはずなのに、最後のログ出力が 300 になっていなかったりするはずです。 increment はインスタンス内部の状態を変化させるメソッドであり、複数のスレッドから呼び出すと予期せぬ挙動をとります。 この部分をクリティカルセクションとして保護してあげれば良いでしょう。単に synchronized をつければよいだけです。

    public synchronized void increment() {
        count++;
        logger.info("count: {}", count);
    }

パフォーマンスのために、一般にクリティカルセクションは小さく保つほうが良いでしょう。

適用可能性

  • 複数のスレッドからクラスへのアクセスが行われるとき
    • シングルスレッドで実行するプログラムにはこのような対応は必要ない。
  • クラスが状態変化する可能性を持っているとき
  • 安全性を保つ必要があるとき

余談

long と double はアトミックに扱われない

Java の primitive 型の参照や代入は基本的にアトミックです。 しかし、longdouble に関してはアトミックではありません。

例えば、同じ long フィールドに対して、異なる 2 つのスレッドから同時に 5678, 1234 という形で代入をおこなったとします。 するとこのフィールドがどのような値をとるか保証されません。この場合は synchronized を使うか volatile キーワードを使うという手があります。

まとめると

  • 基本型・参照型の参照・代入はアトミック
  • long, double に関しては例外
  • long, double をスレッド間で共有する場合は synchronizedvolatile を使う

計数セマフォ

synchronized を使うと、ある領域を 1 つのスレッドのみが実行できるように制限できますが、 n 個のスレッドのみが実行できるようにしたいなどという場合には計数セマフォというものが用意されている。

使い方は簡単で次のような感じ

class BoundedResource {

    private final Semaphore semaphore;

    BoundedResource(int permits) {
        this.semaphore = new Semaphore(permits);
    }

    void use() throws InterruptedException {
        semaphore.acquire();
        try {
            // do something
        } finally {
            semaphore.release();
        }
    }

}
Copyright © 53ningen.com