RSS RSS feed | Atom Atom feed

Java の volatile まとめ

揮発性変数

アトミック変数とか言われたりしますが、ここでは揮発性変数という呼び名で統一します。揮発性変数とは、メインメモリに格納されることを保障された変数のことです。じゃあメインメモリ以外のどこに格納するのさって話なんですが。メモリといえども色々あります(私はあんまり知りませんが)。プロセッサのキャッシュとかレジスタとか、なんかそういうやつです。メインメモリ以外のメモリを、ローカルメモリといいます。ローカルメモリに格納されたデータは、それを格納したプロセッサの他のプロセッサからは見えません。

これで何がうれしいのか。あるスレッドによって書き込まれた値が、次に read するスレッドはその値を読む、ということを保証することです。メインメモリ以外に値が保持されるとこうはいかないわけです。例えばプロセッサのローカルキャッシュに保持されると、一時的にスレッドローカルみたいな感じになってしまうと。

volatile とは

お察しの通り、volatile とは変数を揮発性変数にするための修飾語です。volatile にしとけばメインメモリに格納されることが保障されるわけです。

Java メモリモデル

Java にはメモリモデルという、低レイヤーでの決まりごとがあります。事前条件というものをツラツラと並べた規約です。事前条件ってのは、「XXを行なう前には必ずYYをしなければならない」といったものです。メモリモデルという名前の由来は、プロセッサにおけるメモリモデルからもらってるっぽいです。プロセッサにもメモリモデルがあります。こっちのメモリモデルは名前の通り、メインメモリへのアクセスに対する規約です。データを使う前には必ずデータを読む処理をしなければならないとか、そういうお話だったと思います。詳しくは知りません。

順序変え

Java が要求しているコンパイラ/実行環境は、逐次処理において実行結果が変わらない環境です。つまり、逐次処理において実行結果が変わらなければ、コンパイラや実行環境は、何をしてもいいわけです。この規約がなければ、コンパイラ/実行環境は最適化アルゴリズムを適用することが難しくなります。率直に言えば速度が出しにくくなるのです。そのトレードオフと思えば安いもんです。実行結果は変わらないのだから。順序変えとは、この規約に沿ってコンパイラ/実行環境が実行順序を入れ替えることです。

逐次処理において実行結果が変わらないということは、並列処理では実行結果が変わることがあるということです。実行結果が変わらないように順序変えした複数の逐次処理を並列に走らせた結果、予期しない結果を生むことがあります。例えば、次のプログラムは順序変えの影響をもろに受けるであろう、だめだめなプログラムです。

    private boolean b = false;
    private Object data;

    public void callByThreadA() {
        data = heavyProcess();
        b = true;
    }

    public int callByThreadB() {
        while (!b) {
            sleep(10);
        }
        return data.hashCode();
    }

突っ込みどころが色々あるのはさておき。

ここで問題にしているのは、boolean フラグと heavyProcess が順序変えされてしまい、正しい data がはじき出される前にdata.hashCode が呼び出されることがあるということです。callByThreadA をシングルスレッドだけで実行されるとすれば、(heavyProcess で b が使用されない限り)適当に順序を変えても問題ないということが分かるでしょうか。うそだろーと思いっちゃいますが、起こりえることです。

そこで、並列処理の実行結果を最低限保証するための、順序変えをしてはいけないというケースが欲しくなってきます。この程度のことすら一生懸命同期化しないといけないとなると、ややこしすぎてマルチスレッドプログラミングなんてできません。

そういった順序変えをしてはいけない様々な規約をまとめたのが Java メモリモデルです。

Java メモリモデルにおける volatile の扱い

Java メモリモデルには、volatile に関する規約があります。これは volatile 変数の、もう一つの大きな側面です。一つはメインメモリに格納されることを保障するというやつです。

volatile 変数に対する操作とその他の操作の順序変えを行なってはいけないということを、 Java メモリモデルでは規約しています。これを念頭にさっきの例の boolean フラグを volatile にしてみましょう。b = true と heavyProcess の間で順序変えされることがなくなるので、問題は起こらないでしょう。

volatile の使用目的は、主にこの規約を用いたい場合です。要は順序変えしてほしくない変数を volatile にするわけです。最も多いのは boolean フラグを volatile にすることでしょう。例えばさっきの例は volatile を boolean フラグに使わないとどうなるか、という例です。

旧 Java メモリモデルにおける volatile の扱い

Java メモリモデルは Java 5 で大きく変更が加えられました。実はさっきの volatile の扱いも、Java 5 で追加された項目です。Java 1.4 以前では違いました。Java 1.4以前では、volatile 変数同士の順序変えをしてはいけないという規約でした。一見問題なさそうですが、大有りです。さっきの例で boolean フラグを volatile にしましたが、data が volatile ではないため順序変えが起こってしまいます。要は Java 1.4 以前のvolatile は、全くと言っていいほど役立たずだったわけです。なぜ Java 5 まで渋られたのか。これは順序変えの制約が大きくなってしまい、コンパイラ/実行環境による最適化の幅が大きく狭まることを恐れていたのでしょう。しかしこのようなvolatile の使い方は昔から一般的で(意味ないんだけど)、苦情も多かったんでしょう。大きく言語仕様が変わった Java 5 のリリースと共に、メモリモデルにもメスを入れたわけです。

過信は禁物

なんでもかんでも volatile にしたところで、スレッドセーフ性はあんまり保障されません。というのも、順序変えがないことや可視性(すぐに他のスレッドから値が見えること)を持つという特徴があるだけで、複合操作のアトミック性は確保しないからです。例えば次のようなコード。

    private volatile int x;
    private volatile int y;

    public void unsafe(final int a, final int b) throws InterruptedException {
        x = 0;
        y = 1;
        Thread a = new Thread(new Runnable() {
            public void run() {
                x = x + a;
                y += b + x;
            }
        });
        Thread b = new Thread(new Runnable() {
            public void run() {
                y = y + b;
                x += a + y;
            }
        });
        a.start();
        b.start();
        a.join();
        b.join();
    }

例えばこのメソッドに a=2, b=3 を渡したとしましょう。期待する結果を a の後に b が実行された結果だとすると、x=9, y=13 が正しい結果です(面倒ですがトレースするとそうなります)。まぁしかし、このプログラムは正しく動かないでしょう。x と y が volatile になっていて順序変えがないとはいえ、それは a と b それぞれを逐次処理として見た時のお話です。a と bの run 内部を順序変えすると結果は変わります。しかし正しく同期化しない限り、それを並列にどう実行しようと環境は知ったこっちゃないのです。

この場合は b が y == a + b + x + y を満たすまで待機して、a の処理が終わったらそれを通知するのがベターですかね。

それから

    ++hoge;

なんて処理も volatile ではスレッドセーフ性を確保できません。理由はまた今度。

java.util.concurrent.atomic.*

volatile をさらに押し進めて、明示的なブロック無しに基本的な操作のアトミック性を保証するクラス群のパッケージです。Java 5 で追加されました。目玉は Compare And Swap っていうノンブロッキングアルゴリズムなんですが、中身はcom.sun.Unsafe でした。ソース見れませんでした。こいつらを使えば、意外と面倒なインクリメント/デクリメントのスレッドセーフ性を簡単に構築できたり、色々いいことがあります。大いに使いましょう。

最後に

ここまで来て自白します。実は Java メモリモデルとか、volatile の扱いとか、今ひとつ分かってません。だめじゃん。色々調べて、たぶんこうだろう、というのをツラツラと並べてみました。特にメモリモデルはかなり重要みたいだけど、難しくてなかなかぴんときません。なんか間違いあったら教えてください。



コメント追加 トラックバック送信