ハードウェアの効率と一貫性
マルチスレッド処理と計算効率向上には密接な関係があるが、両者の間には根本的なギャップが存在する。計算処理は演算操作だけでなくメモリI/Oも含まれ、これらの処理速度には数桁の差がある。この差を埋めるため、現代コンピュータシステムは多段階の高速キャッシュを採用している。しかしこれにより、メモリ一貫性という新たな課題が生じる。
各プロセッサは独自の高速キャッシュを持ちながら、同じメインメモリを共有する。さらにプロセッサは内部ユニットを最大限活用するため、入力コードをアウトオブオーダー実行し、結果を再構成して順次実行と同じ結果を保証する。このため、他タスクの中間結果に依存する計算では、コード順序だけで正確な実行順序を保証できない。
Javaメモリモデルの構造
メモリ間相互作用
Javaメモリモデル(JMM)では、全ての変数はメインメモリに格納され、各スレッドは固有の作業メモリを持つ。作業メモリには使用変数のメインメモリコピーが保存され、変数操作は全て作業メモリで実行される。異なるスレッドは互いの作業メモリに直接アクセスできず、変数値の受け渡しはメインメモリ経由で行われる。
主なメモリ操作:
- lock: メインメモリ変数を対象に、スレッド排他状態を設定
- unlock: ロック状態の変数を解放
- read: メインメモリから作業メモリへ変数値転送
- load: readで取得した値を作業メモリ変数に格納
- use: 作業メモリの値を実行エンジンに渡す
- assign: 実行エンジンから受けた値を作業メモリ変数に設定
- store: 作業メモリの値をメインメモリへ転送
- write: storeで取得した値をメインメモリ変数に格納
現代の実装ではread/write/lock/unlockの4操作に簡略化されているが、基本設計は変わらない。
volatile変数の特別な規則
volatileはJavaで最も軽量な同期メカニズムであり、2つの特性を持つ:
- 全スレッドに対する可視性の保証(変数変更が即時反映)
- 命令の再並べ替え防止
通常の変数では、スレッドAの変更がスレッドBに反映されるには、メインメモリへの書き込みと読み取りが必要となる。
public class ConcurrencyDemo {
private static volatile AtomicInteger counter = new AtomicInteger(0);
public static void increment() {
counter.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
Thread[] workers = new Thread[20];
for (int i = 0; i < 20; i++) {
workers[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increment();
}
});
workers[i].start();
}
for (Thread worker : workers) {
worker.join();
}
System.out.println(counter.get());
}
}
元のint型変数では非原子性のため不整合が発生する。以下はcount++の疑似バイトコード:
0: getstatic #14 // フィールドcountの値を取得
3: iconst_1 // 定数1をスタックにプッシュ
4: iadd // 加算実行
5: putstatic #14 // 結果をcountに保存
この処理中に他のスレッドが値を更新すると、競合状態が発生する。AtomicIntegerはこの問題を解決する。