マルチスレッド環境でlongを共有するときの注意

セント セバスティアン グランクリュ

このプログラムは止まるだろうか?
このスレッドは、lockの上位32ビットと下位32ビットが異なると止まる。mainでは、lockに同じ乱数を上位32ビットと下位32ビットに設定する。
だから、プログラムが書いた通りに動けば止まらないはず。

import java.util.Random;

public class VolatileSample {
    static long lock = 0;
    public static void main(String[] args){
        new Thread(){
            @Override public void run() {
                for(;;){
                    long t = lock;
                    long high = t >> 32;
                    long low = t & 0xffffffffL;
                    if(high != low){//上位32ビットと下位32ビットが等しいか判定
                        System.out.println("おわた");
                        System.exit(0);
                    }
                }
            }
        }.start();
        Random r = new Random();
        for(;;){
            long a = r.nextInt();
            if(a < 0) a -= Integer.MIN_VALUE;
            //上位32ビットと下位32ビットに等しい値を設定
            long t = (a << 32) + a;
            lock = t;
        }
    }
}


このプログラムは、32ビットJava VMであれば結構早く止まる。試してないけど、64ビットJVMだと止まらないはず。
32ビットJVMでは、long値を設定するときに、上位32ビットと下位32ビットが別々に設定される。そうすると、スレッド内でt=lockとしているとき、上位(もしくは下位)だけが変更された中途半端な値を読むことがある。そのとき、このプログラムは止まる。


ここで、lockをvolatileにすると、止まらない。

volatile static long lock = 0;

volatileにすると、longの変数をアクセスしているとき、読み込み・書き込みが終わるまでスレッドがロックされる。
volatileの役割は、簡単にいうと、ちゃんと書いたように動け、という指示だ。変な最適化をするな、と。今回の場合は、lock変数にアクセスするときはシンクロナイズドしろということになる。通常は、遅くなってしまうのでシンクロナイズドしない。


ただし、次のようにしてしまうと、highの値を設定したあとlockが設定されてlowには別の値が設定されることがあるので、lockがvolatileでもこのプログラムは止まる。

long high = lock >> 32;
long low = lock & 0xffffffffL;


追記
id:yokoletさんから、-serverつけると止まらないと教えてもらったので、ためしにやってみたら止まりませんでした。
でも、これって、runメソッド一回しか呼ばれてないからHotSpot最適化されてないんじゃないかと思って、メソッド別にして試してみました。

        new Thread(){
            @Override
            public void run() {
                for(;;){
                    foo();
                }
            }
            void foo(){
                for(int i = 0; i < 100; ++i){
                    long t = lock;
                    long high = t >> 32;
                    long low = t & 0xffffffffL;
                    if(high != low){
                        System.out.println("おわた");
                        System.exit(0);//fin = true;
                    }
                }
            }
        }.start();

そうすると、やっぱり終わってしまいました。