WisdomSoft - for your serial experiences.

9.3 volatile変数

複数のスレッドで共有される変数の読み取りと書き込みの関係と volatile 修飾子による変数の同期について説明します。

9.3.1 作業メモリとマスタ・コピー

高水準言語を利用するプログラマは本来意識する必要はありませんが、マルチスレッドを使う場合はスレッドがどのようにメモリを利用しているかを知らなければなりません。なぜならば、スレッドは共有する記憶領域とは個別に、それぞれが独立した作業領域を保有しているためです。

すべてのスレッドが共有する領域をメイン・メモリと呼び、各スレッドが個別に保有する専用の作業領域を作業メモリと呼びます。スレッドは変数を利用する時、メイン・メモリに配置されているマスタ・コピーを作業メモリに複製して、これを操作するのです。これを作業コピーと呼びます。マスタ・コピーと作業コピーのやり取りは厳密に仕様で定められており、マルチスレッドの場合はこの関係が重要になることがあります。

スレッドが作業コピーの値を変更すれば、その結果は最終的にマスタ・コピーに転送されます。メイン・メモリのデータがどのように流れ、読み込みや書き込みが行われるかについては、仮想マシンやアセンブリ言語の世界となるため割愛します(Java 仮想マシン仕様Java 言語仕様を参照してください)。

Java 言語プログラマにとって重要なのは、複数のスレッドで共有している変数が他のスレッドから変更されたとき、共有している変数への変更が作業メモリに留まったままで、マスタ・コピーに移されていない可能性がるということです。別のスレッドから変数を読み込んだとき、変数の値は古いままかもしれません。

変数に対するスレッドからの要求順に処理されるようにするには  volatile 修飾子をフィールド宣言で指定します。volatile 修飾子が指定されている変数は、スレッドがアクセスする毎にフィールドの作業コピーとマスタ・コピーを一致させます。

※2018/7/27 追記: メールでご指摘いただきました。以下のサンプルコード、volatile の説明になっていません。詳しくは時間ができたときに別記します。

コード1
class Test extends Thread {
	public static void main(String args[]) {
		for(int i = 0 ; i < 5 ; i++) new Test().start();
	}

	static volatile int iValue1 , iValue2;
	private static void add() { iValue1++; iValue2++; }
	public static void show() {
		System.out.println(
			Thread.currentThread().getName() +
			":iValue1=" + iValue1 + ",iValue2=" + iValue2
		);
	}
	public void run() { 
		for(int i = 0 ; i < 100 ; i++) {
			add();
			show();
		}
	}
}
実行結果
>java Test
Thread-1:iValue1=1,iValue2=1
Thread-1:iValue1=2,iValue2=2
Thread-1:iValue1=3,iValue2=3
Thread-1:iValue1=4,iValue2=4
Thread-1:iValue1=5,iValue2=5
Thread-1:iValue1=6,iValue2=6
Thread-1:iValue1=7,iValue2=7
Thread-1:iValue1=8,iValue2=8
Thread-1:iValue1=9,iValue2=9
Thread-1:iValue1=10,iValue2=10
Thread-1:iValue1=11,iValue2=11
Thread-1:iValue1=12,iValue2=12
以下省略...

コード1では、Test クラスのクラス・メソッド add() と show() が複数のスレッドから無秩序に呼び出されます。実行結果は実行環境によって異なるため不定です。もしスレッドが 1 つしか存在しないのであれば、add() と show() をどのような順序で呼び出しても iValue1 と iValue2 の値は常に等しいと考えられます。しかし、複数のスレッドから無秩序に更新された場合、iValue1 と iValue2 の値が異なる状態で画面に表示される可能性があるのです。それは、コンパイラがどのように最適化するかという不安定な問題であり、このような地雷を残すのは危険です。

そこで iValue1 と iValue2 の宣言に volatile 修飾子を指定します。これを指定すれば、スレッドがフィールドを参照すると、作業コピーとマスタ・コピーを一致させます。volatile はスレッドの作業コピーと、メイン・メモリのマスタ・コピーを同期させる修飾子であると覚えてください。このような、目に見えない低水準な概念は難しく思えるでしょう。

複雑なマルチスレッド・プログラミングはほとんどの場合で避けることができるため、できることならこうしたプログラムは避けたほうが無難かもしれません。優秀なプログラマは、複雑な問題を単純化することの重要さを理解しています。単に複雑で難しいプログラムを作れることは、褒められるものではありません。