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

volatileってなんだろう

今日の一歩 java

目的

「volatileって何?」という質問があり、自分の理解も曖昧だったので復習してみた。

勉強内容

  • volatileって?
  • 何の意味があるの?
  • コンパイラの最適化(置き換え)を抑止する
  • コンパイラの最適化(リオーダー)を抑止する
  • スレッドが値を参照する際に、必ず最新の値を見るようにする
volatileって?

修飾子です。次のようにフィールドに対して付けます。

public class Hoge {
    public volatile int num;
}
何の意味があるの?

ぼくは次のように理解していますが、まだ自信はありません。

  • コンパイラの最適化(置き換え)を抑止する
  • コンパイラの最適化(リオーダー)を抑止する
  • スレッドが値を参照する際に、必ず最新の値を見るようにする

Javaでは3つ目の説明ばかり見るのですが、これは結果として他のも対応されるからなのかな?
まだよく分かっていません。

コンパイラの最適化(置き換え)を抑止する

次のような疑似コードがあるとします。

boolean flag;
function something() {
  while(flag) {
    // flagには一切関係ない処理
  }
}

上記ではwhileの条件としてflagを参照しています。
処理の中ではflagを操作しないため(値が変わらないため)、whileで毎回flagを判断するのは無駄に見えます。
なので、上のコードは次のように書き換えられます。

boolean flag;
function something() {
  if(flag) {
    while(1) {
      // flagには一切関係ない処理
    }
  }
}

説明のためにとても簡単な例としましたが、要は「無駄な評価をなくすようにコンパイラがよしなにやってくれる」わけです。
これにより間抜けなコードを書いたとしても、賢いコンパイラさんによって性能が向上するわけです。

ただ、これが嬉しいのはシングルスレッドで動いている場合です。
マルチスレッドにてflagの値が変更される可能性がある場合、このように最適化されては永遠に止まらずバグへ繋がります。

そうならないために、「最適化しないでね」という印を付けるのがvolatile修飾子なわけです。
これが1つ目。

コンパイラの最適化(リオーダー)を抑止する

プログラムの実行順序を変更する最適化を「リオーダー」と呼びます。
実行結果が変わらなければOKとされます。
疑似コードを見てみます。

// 1と2を入れ変えると結果が変わるのでNG
int x;
int y;
function ng() {
  x = 1; // 1
  y = x; // 2
}

// 1と2を入れ変えても結果は同じなのでOK
int x;
int y;
function ok() {
  x = 1; // 1
  y = 1; // 2
}

マルチスレッドの場合、リオーダーされるとバグを生む可能性があります。
詳しくはこちらのスライド13枚目から。
そろそろvolatileについて一言いっておくか

このリオーダーを抑止する役目もvolatileにあります。
ただし、volatile変数とそうでない変数では抑止対象になりません。

// これはNG。リオーダーされるかも
volatile int x;
int y;
function ng() {
  x = 1;
  y = 2;
}

// リオーダー抑止!
volatile int x;
volatile int y;
function ok() {
  x = 1;
  y = 2;
}
スレッドが値を参照する際に、必ず最新の値を見るようにする

Javaにおいてはこれがメインなのかも。
色々なサイトが分かりやすく説明してくれているので、いくつか引用します。

マルチスレッドの場合、それぞれのスレッドは性能向上のために変数のコピーを参照・変更し、その値を元の場所(メモリ)に書き戻さないことがあります。つまり、同じ変数でもスレッドによって値が異なるという現象が発生します。複数のスレッドから参照される可能性のある変数に volatile をつけることにより、この問題を回避することができます。
とほほのJava入門

Java では生成されたすべてのスレッドは メインメモリ を共有しますが、 処理の最適化などの理由で、メインメモリの内容を個々のスレッドが保持している 作業メモリにコピーして処理を行うことがあります。

このため 作業メモリ と メインメモリ の間で不整合が生じる可能性があり、もしその変数が複数の スレッドからアクセスされるような場合には、値の参照が正しく行われない可能性があります。

こうした不整合が生じるのを防ぐためにJavaではvolatileという修飾子が用意されています。
volatileとは:SJC-P対策Java用語集

実はクラスのフィールドには複数のコピーが存在します。
その存在場所がメインメモリと作業用メモリです。
インメモリにあるものがマスターコピーであり、
スレッドごとに存在する作業用メモリ上にそのコピーが置かれます。
フィールドの読み書きはこの作業用メモリにあるコピーに対して行われ、
適宜マスターコピーとの間で同期が取られます。
これは実行効率を上げるために行われています。
(以下略)
volatileの振る舞いが分かりません。 - Java - 教えて!goo

箇条書きにしてみます。

  • スレッド間で値を共有するのはメインメモリと呼ばれる場所である
  • 実行効率をあげるため、各スレッドはそれぞれ作業用メモリを持っている
  • 基本は作業用メモリに対して読み書きが行われていて、いい感じのタイミングでメインメモリと同期される
  • インメモリへの同期が遅れている場合、別スレッドが参照している値は古い可能性がある

volatileを付けることで、値を読み取るときに必ずメインメモリを参照することが保証されます。
そのため、「作業用メモリの値見てたんだけど、ごめんコレ古かったわw」という事態を回避出来ます。

具体的な動きの例については下記が分かりやすいです。
Javaスレッドメモ(Hishidama's Java thread Memo)

感想

メモリモデルとかメモリバリアとかの単語、概念に触れてちょっと理解したくなった。
そのあたりをちゃんと理解しないと、volatileの理解も「なんとなく」のままだなあ。

あと、図を書いてアップしたいな。文章だと分かり辛い。
インメモリと作業用メモリのとことか、図を書いた方が絶対分かりやすい。