プロが教えるわが家の防犯対策術!

Javaのプログラミングを行っているのですが、volatileをつけた結果になっとくがいきません。
volatileというのは、他のスレッドによって変数の値が書き換えられる状態になるのを防ぐことができる
とあるのですが、出力結果からしてそうじゃないんじゃないかと考えています。
下記のソースの実行結果がなぜそうなるのかいまいち分かっていません。

public class Thirsty {
//ボトル
static Bottle bottle;

public static void main(String[] args) {
// TODO 自動生成されたメソッド・スタブ
bottle = new Bottle(2000);

// ボトルから水を飲む人
class Person extends Thread {
public Person(String name) {
super(name);
}
public void run() {
// 200ml飲んでみる。飲めたらtrueが返される
while(true) {
if(bottle.drink(10)) {
System.out.println(Thread.currentThread().getName() + " did drink water ");
} else {
System.out.println(Thread.currentThread().getName() + " couldn't drink water ");
break;
}
}
}
}

Person yamada = new Person("Yamada");
Person tanaka = new Person("Tanaka");
yamada.start();
tanaka.start();
}
}

class Bottle {
private volatile int water;// volatileを使用した場合
Bottle(int amount) {
water = amount;
}

// ボトルにamountで指定した量だけ残っているか
private boolean contains(int amount) {
if(amount <= water) {
return true;
} else {
return false;
}
}

// ボトルの水をamountで指定した量だけ水を飲む
public boolean drink(int amount) {
if(contains(amount)) {//水があれば残りの量から引く 残量を出力する
water -= amount;
System.out.println(Thread.currentThread().getName() + " water = "+water);
return true;
}
return false;
}
}


私の場合、出力結果で、最後の方には
Yamada did drink water
Yamada water = 0
Yamada did drink water
Yamada couldn't drink water
Tanaka water = 80
Tanaka did drink water
Tanaka couldn't drink water
となります。 途中でも waterの値がいきなり高くなったりします。
なぜこのような結果がでるのでしょうか?
volatileというのはメインメモリの値を必ず参照するとあるのですが、このメインメモリというのは
class Thirstyのメモリを指すのでしょうか?

A 回答 (4件)

実はクラスのフィールドには複数のコピーが存在します。


その存在場所がメインメモリと作業用メモリです。
メインメモリにあるものがマスターコピーであり、
スレッドごとに存在する作業用メモリ上にそのコピーが置かれます。
フィールドの読み書きはこの作業用メモリにあるコピーに対して行われ、
適宜マスターコピーとの間で同期が取られます。
これは実行効率を上げるために行われています。

volatileなフィールドの場合はマスターコピーがフィールドの読み書きでの操作対象になります。
> volatileというのはメインメモリの値を必ず参照する
というのがまさにこれで、マスターへのアクセスが行われることが保証されます。
> volatileというのは、他のスレッドによって変数の値が書き換えられる状態になるのを防ぐことができる
わけではなく、
どのスレッドも確実にそのフィールド(のマスターコピー)をその時点で書き換えることを保証するということです。
したがって、volatileは同期制御が不要になる魔法の呪文ではありません。
どのスレッドも自分の好き勝手なタイミングでフィールドを書き換えるのはvolatileを付けても同じです。
ただ、作業用コピーに対して行い同期はシステムに任せるのか、直接マスターコピーに対して行うのかの違いだけです。
質問者のサンプルでいえばcontainsとdrinkにsynchronizedを付ける等、
同期を取る仕組みを加えないとおかしなことになります。

volatileは最適化によってマスターコピーが変更されない事態を防ぐことの他に、
double型やlong型のようなメイン-作業間でのアトミックな扱いが保証されていないような型のフィールドを
マルチスレッドで扱えるようにするためにもあります。
64ビット幅のdouble型やlong型はマスターコピーと作業用コピーとの間でやりとりする時に、
一度に同期が取られることが保証されていません(非アトミックな扱い)。
32ビット幅分ずつで2回でコピーされうるので、
例えばあるスレッドから32ビット分だけ作業用コピーからマスターへコピーされた時点で、
他のスレッドによってマスターコピーが読まれるような事態が起こりえます。
volatileはこれを防ぐことができ、
全幅64ビット分の同期が終了しない限り他のスレッドからアクセスされなくなります。
この機能が
> volatileというのは、他のスレッドによって変数の値が書き換えられる状態になるのを防ぐことができる
という誤解を生んでいるんじゃないかなとも思ったりします。

volatileについてはJava言語仕様の他、Java仮想マシン仕様に詳細があるので、
これらの文書の中をvolatileをキーワードに探してみてください。
    • good
    • 1
この回答へのお礼

回答ありがとうございます。

大変参考になりました。
サイトを見て周ってもあまりよく分からなかったのでここで質問をしました。
volatileとメインメモリ、作業用メモリとの関係が理解できました。
volatileではスレッドの割り込みを防ぐことはできなんですね。

お礼日時:2011/09/17 12:06

他の方も書いているとおり、


>volatileというのは、他のスレッドによって変数の値が書き換えられる状態になるのを防ぐことができる
というのが全くのでたらめです。

int sum=0; int x=0; int i;
for(i=0;i<10000;i++){
xやsumに関係ない他の処理;
sum += x;
}

は、おそらく、
int sum=0; int x=1; int i;
for(i=0;i<10000;i++){
xやsumに関係ない他の処理;
}
sum=10000;
に最適化されます。ところが、x が他のスレッドで書き換わるなら、毎回xの値を調べて足し込む必要があります。その場合はvolatileをつけます。
「xは他のスレッドで書き換わる可能性があるよ」ということをコンパイラに知らせるのか゛volatileキーワードの役割です。

>volatileというのはメインメモリの値を必ず参照するとあるのですが、

変数の値を毎回参照すると言うことです。
    • good
    • 0
この回答へのお礼

詳しい説明ありがとうございます。
volatileの理解がまた一段と深まりました。
volatileの扱いはまだこれからなので大変参考になります。

お礼日時:2011/09/17 12:14

少し訂正。


> この機能が
>> volatileというのは、他のスレッドによって変数の値が書き換えられる状態になるのを防ぐことができる
> という誤解を生んでいるんじゃないかなとも思ったりします。
作業用コピー間の同期という点では割り込まれないことが保証されているので誤解ではないですね。
正確には、だから同期処理を行わなくてもいいというのが誤解ですね。
質問者のサンプルでいえば、drinkの中で、
water -= amount;
の箇所がwaterがvolatileであってもアトミックには行われない(waterの値を読み出すことと引いた値を代入することは分割されうる)ことや、
containsの実行とwaterを減らす処理の間に状況が変わりうることに対する手当ては必要ということです。
    • good
    • 0
この回答へのお礼

drinkの部分にはアトミックには行われない(作業用コピー間)ではない
ということですね。

お礼日時:2011/09/17 12:09

≫volatileというのは、他のスレッドによって変数の値が書き換えられる状態になるのを防ぐことができる



私の解釈では、どちらかというと逆の意味になると思います。
「他のスレッドによって変数の値が書き換えられる可能性があるのでコンパイラにより最適化されるのを防ぐことができる」

たとえば次のコードがあったとして
int a, b, c;
a = 1;
b = 2;
c = a + b;
これをコンパイラは3行まとめて「c = 3」という最適化をしてしまいます(その方が実行速度が速くなりバイナリも小さくなるので)。

で、int volatile a; と宣言がされたなら、コンパイラは「c = a + 2」という最適化に抑えられ、cに代入する段階でaの値を必ずメモリ上から読み取る、ということになります。
(極端な例なので必ずこうなるかはわかりません。私の知識上の想像です)

マルチスレッドや特殊な割り込み処理では、フラグなどがいつ更新されるかがとても重要であり、最適化されると困るためにvolatileという修飾子が必要になります。

この回答への補足

コンパイラの最適化を防止するというのは他のサイトでも載ってありました。
しかし、このコードの出力結果とvolatileの関係が理解できなかったのです。

最後の出力結果の状態は、
Yamadaでwaterが0になるまえにTanakaに割り込まれ、waterの表示が0
なのにTanakaのwaterは80あるという事なんでしょうか?
再度試してみたところ、volatileが有る無し関係なく、割り込まれるようです。
ただvolatileがあると割り込まれやすいようです。

補足日時:2011/09/16 14:47
    • good
    • 0
この回答へのお礼

回答ありがとうございます。

お礼日時:2011/09/16 14:48

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!