谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

volatileとか使うなと怒られた話

JJUG Night Seminar ~ Java VM<&納涼会 ~
http://kokucheese.com/event/index/48437/
に参加してきました。


「スタックマシンとしてのJavaVM」なんて言う
一見さんお断りみたいなタイトルに集まった人たちはもちろんレベルが高く
参加者のjavap経験率が20%ぐらいになるなど、なかなか偏った集客具合でした。


そんな中、私もLTでBTraceの話をしようと、45枚のスライドと、2種類のデモを用意していました。
ただ客層を考えるに、最近Twitterでちょっと呟いていた
「longに複数スレッドから値を代入すると、想定外の値になる」話をした方が良いんじゃないかと思い、
観客にどちらが聞きたいか尋ねてみたところ、longの方が圧倒的人気。


そんなわけで、45枚のスライドをドブに捨てて(いつか再利用するよ!)
スライドなしで、longの話をしてきました。

longに1と-1を入れ続けると、4294967295や-4294967295になる

論より証拠、まずはWindowsXP 32bitなど、
普通の32bit OSで、以下のコードを実行してみてください。

public class VolatileTest {
	private static long longValue = 0;

	public static void main(String[] args) throws Exception {
		final int LOOP = 1000 * 1000 * 1000;

		Thread th1 = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < LOOP; i++) {
					longValue = 1;
					check(longValue);
				}
			}
		});

		Thread th2 = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < LOOP; i++) {
					longValue = -1;
					check(longValue);
				}
			}
		});

		th1.start();
		th2.start();

		th1.join();
		th2.join();

		System.out.println("Finished");
	}

	private static void check(long value) {
		if (value != 1 && value != -1) {
			throw new RuntimeException(String.valueOf(value));
		}
	}
}

2スレッドからlongValueに1と-1を入れ続け、
もし値が1と-1以外になった場合には例外を発生させる、というプログラムです。


素直に考えると「1」と「-1」以外になることはあり得ないのですが、
実際には「4294967295」や「-4294967295」になることがあります。

64bitなlongへの代入は、上位32bitと下位32bitに分けて行なう

なぜそのような事が起こるのでしょうか?
それは、longへの代入がアトミック(分割できない最小単位)ではないためです。


見出しに書いた通りですが、Javaのlongは64bitであり、
特に32bit版のJavaVMでは、上位32bitと下位32bitの代入は別々に行なわれるのです。


つまり上に書いたプログラムの代入部分は、こんな風に読み替えることができます。

// Thread1側 (longValue = 1)
longValue_upper32 = 0x00000000;
longValue_lower32 = 0x00000001;

// Thread2側 (longValue = -1)
longValue_upper32 = 0xFFFFFFFF;
longValue_lower32 = 0xFFFFFFFF;

この処理をマルチスレッドで動かすわけですから、値の状態として
 longValue_upper32 = 0x00000000;
 longValue_lower32 = 0xFFFFFFFF;
とか
 longValue_upper32 = 0xFFFFFFFF;
 longValue_lower32 = 0x00000001;
とかになることは、容易に想像ができます。


その結果、
 前者の0x00000000FFFFFFFFが「4294967295」
 後者の0xFFFFFFFF00000001が「-4294967295」
になるのです。


ただしこの問題、64bitなMac OSJavaでは発生しません。
少なくとも私のMacBook Air(Lion)では、代入を上位下位に分割せず
64bit分を一度に代入するため、常に1か-1になります。

そこでvolatileの登場です

「ここで出てくるのがvolatileです」
って言ったら櫻庭さんから「あり得ない!」っていきなり怒られたのですが(><)
めげずに説明を続けますと

private volatile static long longValue = 0;

と、volatileで宣言すれば32bit Javaでも問題は起きません。


volatileには「値の参照の際に、常に最新の値を見に行く」という性質があり、
ざっくり言えば、volatileの代入や参照はロックされているような振る舞いをします。


そのため、longValueの値を上位32bit、下位32bitともきちんと処理し終わってから
他のスレッドからlongValueの参照ができるようになります。


なんだかよく分からない修飾子ナンバーワンのvolatileですが、
この例を通して見て頂ければ、その振る舞いがよく分かるのではないかと思います。

でもホントはAtomicLongを使う。

とは言え、
実際にどう振る舞うかが保証(規定)されていないvolatileを
このような場所で使うのは、必ずしも安全とは言えません。


また、よく分からない修飾子ナンバーワンなのですから、
よく分からないままコピーされたり、よく分からないまま削除されたり(!)しかねません。


では代わりに何を使うべきかというと、AtomicLongです。

private static AtomicLong longValue = new AtomicLong(0);

AtomicLongのgetやsetは、その名前の通りアトミックに行なわれるため
マルチスレッドで操作しても問題は発生しません。
分かりやすい名前のため、可読性も高いと言えるでしょう。


ところで、
変数をvolatileで宣言しても、インクリメント処理はアトミックにならないため、
複数スレッドからインクリメント処理を行なうと、値が壊れる可能性があります。
(これはlongだけでなく、shortやintなどでも発生します)

private volatile static int intValue = 0;

// 複数スレッドから実行すると値を更新しないことがある。
public void increment() {
	intValue++;
}


そのような場合でも、
AtomicIntegerやAtomicLongのincrementAndGetメソッドを使うことで
インクリメント処理をアトミックに行なうことができるのです。

volatileの使いどころ

そんなわけで使いにくいvolatileですが、
ステートフラグとか、ダブルチェックロッキングとかでは使えますよね!

まとめ

  • 変数への代入が、いつもアトミックだと過信してはいけない。
  • volatile! volatile!
  • 普通にAtomicIntegerやAtomicLongを使ってください。