[Java] volatile 変数

Java(tm) の同期機構:
synchronized ブロック/メソッド
volatile 変数

どちらもスレッド・セーフにコードを描画するためのもの

volatile変数は
「synchronized の軽量版」で
同期の仕組みとしては強力でないが
誤った使い方をされがち

volatile変数に必要なコーディングは
synchronized ブロックよりも少なく
volatile変数の方が実行時のオーバーヘッドも少ない

volatile 変数は
synchronizedの一部しかできない



volatile 変数を効果的に使うためのパターンは…



ロックの基本的特徴
相互排除と可視性

相互排除は
指定されたロックを保持できるのは 1 度に 1つのスレッドのみであることを意味する
この性質を使うと
1 度に 1つのスレッドしか共有データを使用できないように
共有データへのアクセスを調整するプロトコルを実装できる

可視性は
ロックを解放する前に共有データに加えられた変更が
次にそのロックを取得する別のスレッドから見えるように保証するもの
同期によって可視性が保証されないと
スレッドは共有変数の値として
古かったり一貫性のない値を見てしまい得る


volatile 変数は
可視性は synchronized と同じだが
synchronizedのようなアトミック性を持ってない
つまり
スレッドは自動的に volatile変数の最新の値を見る
スレッド・セーフを提供するために volatile変数は使えるが ケースが限定される
つまり
複数の変数の間

1つの変数の現在の値と将来の値との間
に制約が課されない場合のみ使える
そのため
複数の変数を関連付ける“start <=end”等の不変式を持つカウンターや
ミューテックス その他のクラスを実装するには volatileだけでは強力さが不十分


ロックの代わりに
volatile 変数を使いたい主な理由は
単純さかスケーラビリティーだろう

一部のイディオムは
ロックより volatile 変数の方がコーディングしやすく読みやすくなる
また
volatile 変数は
ロックと違い
スレッドをブロックできないため
スケーラビリティーの問題を起こしにくい
書き込みより圧倒的に読み取りが多い場合も
ロックより volatile 変数の方がパフォーマンスが良い



volatile 変数が使えるのは
非常に限定された状況のみ

必要なスレッド・セーフを volatile 変数で提供するためには
下記の両条件を満たせる(他のプログラムの状態には依存しない)変数であるけと

条件① その変数への書き込みが その変数の現在の値に依存しない
条件② その変数が 他の変数との不変式に使われていない

条件①から
volatile 変数をスレッド・セーフなカウンターに使用するのはNG

インクリメント演算 (x++) は 1つの演算のように見えるが
実際には「読み取り、変更、書き込み」という連続した複合演算で
アトミックに実行する必要がある
しかし
volatile 変数には必要なアトミシティはない
演算が適切であるためには
演算中に x の値は不変でないとダメで
volatile 変数では実現できない
ただ
その値が 1つのスレッドからしか書き込めないようにしてあれば
条件①は無視可能

大部分のプログラミング状況は、条件①か条件②に違反するため
スレッド・セーフ実現方法として
volatile 変数使用するのは一般的でない


ケーススタディ:volatile を使えないケース
サンプルクラスは
スレッド・セーフではない 数字範囲のクラスを示すもの
このクラスは不変式を含み 下限は必ず上限以下になる


|@NotThreadSafe
|public class NumberRange {
| private int lower, upper;

| public int getLower() { return lower; }
| public int getUpper() { return upper; }

| public void setLower(int value) {
|  if (value > upper)
|   throw new IllegalArgumentException(...);
|  lower = value;
| }

| public void setUpper(int value) {
|  if (value < lower)
|   throw new IllegalArgumentException(...);
|  upper = value;
| }
|}


範囲の状態変数はこのように制約されているため
lower フィールドと upper フィールドを volatileにするだけでは
クラスをスレッド・セーフにするには不十分で 同期が必要になる
でないと
タイミングが悪いと
setLower と setUpper を実行する (一貫性のない値を持つ) 2 つのスレッドで 範囲に一貫性がなくなる
例えば
最初の状態が((0, 5)) だったとし
スレッド A が setLower(4) を呼び出すと同時に
スレッド B が setUpper(3)を呼び出すと
演算は途中で割り込まれた形で不適切に実行され
どちらの演算も 不変式を守るはずのチェックにパスしてしまう
その結果、
範囲は(4, 3) という無効な値を保持することになる
setLower() と setUpper()の演算を
範囲に対する他の演算との関係から見てアトミックにする必要がある
しかしフィールドを volatile にしても アトミックにはならない


パフォーマンスに関する考慮事項

volatile 変数の利点は
単純さとパフォーマンス


現在の大部分のプロセッサーのアーキテクチャーでは
volatile の読み取りは nonvolatile の読み取りと同じくらいの低コスト
一方
volatile の書き込みは nonvolatile の書き込みよりも大幅に高コスト
これは可視性を保証するためにメモリー・フェンスの動作が要求されるためですが
それでも volatile 書き込みは ロック取得よりは低コスト


volatile 演算はブロック動作を行わない分
ロックよりもスケーラビリティーが優れている
書き込みよりも読み取りが圧倒的に多い場合には
volatile 変数は大抵、ロックと比較して同期のパフォーマンス・コストを低下させる。


volatile を正しく使うためのパターン

プログラム中の他のいかなるものにも全く依存しない時のみ
volatile を使用できる


パターン #1: ステータス・フラグ

1 度だけ発生する重要なライフサイクル・イベントの発生
(初期化完了やシャットダウン要求受付など)
を示す 単純なブール値のステータス・フラグに使う

サンプル
variable 変数をステータス・フラグとして使う

|volatile boolean shutdownRequested;

|...

|public void shutdown() { shutdownRequested = true; }

|public void doWork() {
| while (!shutdownRequested) {
|  // do stuff
| }
|}

shutdown() メソッドはループ外のどこかから (別のスレッドで)呼び出される可能性がある
従って
shutdownRequested変数の可視性を適切に保証するためには何らかの形式の同期が必要になる
しかし
synchronized ブロックを使ってループをコーディングしては
上記のように volatile ステータス・フラグより面倒で複雑なる


このタイプのステータス・フラグの一般的に状態遷移が 1 通りしかない
shutdownRequestedフラグは false から trueになり
プログラムがシャットダウンする


パターン #2: 1 度だけ安全に公開する

同期していない場合には可視性に問題が生じることがあるが
プリミティブ値ではなくオブジェクト参照に書き込む場合には
その問題の原因が一層探りにくくなる
同期していない場合には
オブジェクト参照の最新の値を見たとしても
それは別のスレッドが書き込んだ値であり
そのオブジェクトの状態の古い値を見ている可能性がある
(この最新の参照を見ているにもかかわらず
その参照によって
部分的に作成されたオブジェクトを見てしまうという危険性が
正しくオブジェクト参照が同期することなく読み取られるダブルチェック・ロック・イディオムの根本原因)

安全にオブジェクトを公開するためには
オブジェクト参照を volatile にする

サンプル
起動中にバックグラウンド・スレッドがデータベースからデータをロードする
他のコードは
このデータを利用できそうな場合にそのデータを使おうとする前に
データが公開されていたかどうかを確認する
|public class BackgroundFloobleLoader {
| public volatile Flooble theFlooble;

| public void initInBackground() {
|  // do lots of stuff
|  theFlooble = new Flooble(); // this is the only write to theFlooble
| }
|}

|public class SomeOtherClass {
| public void doWork() {
|  while (true) {
|   // do some stuff...
|   // use the Flooble, but only if it is ready
|   if (floobleLoader.theFlooble != null)
|   doSomething(floobleLoader.theFlooble);
|  }
| }
|}

theFlooble 参照が volatile ではないと
doWork() のコードは
theFlooble 参照を参照解除する際に
部分的にしか作成されていない Flooble を見てしまいうる

ポイントは
公開されるオブジェクトが
スレッド・セーフである

実質的に不変である
※実質的に不変
そのオブジェクトの状態が
そのオブジェクトが公開された後は変更されない ゆうこと

volatile 参照は
公開された形式でのオブジェクトの可視性を保証するだけ
そのオブジェクトの状態が公開後に変化する場合は同期する必要がある


パターン #3: 独立した観測結果

プログラム内で使用するために観測結果が定期的に「公開される」場合にvolatile を使用する

例えば
現在の温度を感知する環境センサーがある場合
バックグラウンド・スレッドは
このセンサーを数秒ごとに読み取り
現在の温度を含む volatile 変数を更新し
他のスレッドは
常に最新の値が見えていることを知った上で
この変数を読み取れる

プログラムに関する統計を収集する場合も同様に volatileが有用

サンプル
独立した観測結果を複数公開する
最後にログオンしたユーザーの名前を認証メカニズムが記憶する
lastUser 参照は
この値を公開してプログラムの他の部分で使用できるように 繰り返し使われる

|public class UserManager {
| public volatile String lastUser;

| public boolean authenticate(String user, String password) {
|  boolean valid = passwordIsValid(user, password);
|  if (valid) {
|   User u = new User();
|   activeUsers.add(u);
|   lastUser = user;
|  }
|  return valid;
| }
|}


公開される volatile 値が実質的に不変であること
(値の状態が公開後変化しないこと)
この値を利用するコードは
この値が随時変化する可能性があることを認識すること


パターン #4: 「volatile bean」パターン

JavaBeans を「見せかけの構造体 (glorified struct)」として使用するフレームワークに適用する
※glorified struct:
メンバーが public で 構造体と変わらないような単純な構成をしたクラス

volatile beanのパターンでは
JavaBeanをゲッターやセッターを持つ独立したプロパティー・グループのコンテナーとして使う

volatile beanのパターンを使うのは
多くのフレームワークが可変データ・ホルダーのコンテナ (HttpSession等)を提供する一方
こうしたコンテナに置かれるオブジェクトはスレッド・セーフでなければならないため
volatile beanのパターンのJavaBeanのすべてのデータ・メンバは volatileで
ゲッタとセッタは単純なものである必要がある

volatile beanのパターンでは
JavaBean のデータ・メンバは全てvolatileで
ゲッタとセッタは単純なものであること
※ゲッタとセッタには
適当なプロパティを取得/設定するためのロジック以外は何も含まんこと
また
データ・メンバが参照するオブジェクトも実質的に不変であること
※配列の値を持つプロパティは持てない
配列参照が volatile と宣言されると
要素そのものではなく参照のみしか
volatile として認識されないため

サンプル
volatile beanのパターンに従う Person オブジェクト

|@ThreadSafe
|public class Person {
| private volatile String firstName;
| private volatile String lastName;
| private volatile int age;

| public String getFirstName() { return firstName; }
| public String getLastName() { return lastName; }
| public int getAge() { return age; }|

| public void setFirstName(String firstName) {
|  this.firstName = firstName;
| }

| public void setLastName(String lastName) {
|  this.lastName = lastName;
| }

| public void setAge(int age) {
|  this.age = age;
| }
|}



□volatile の応用パターン
volatile を使うことでパフォーマンスやスケーラビリティーを高められるケース

ごくわずかな変更でもコードを実行できなくなる可能性があるため
前提条件に注意を払い
パターンを強力にカプセル化しておくことが大切になる
また
パフォーマンス向上の可能性と引き換えに
可読性と保守性が犠牲になる

パターン #5: 安価な読み書きロックの秘訣

volatile はカウンタを実装するのに力不足
++x も実際は 3つの演算 (読み取りと追加と保存) の簡略表現で
複数のスレッドが volatile カウンタを同時にインクリメントしようとして
タイミングが悪いと更新に失敗しうる

しかし
変更よりも読み取りが圧倒的に多い場合は
本来のロックと volatile 変数を組み合わせることで
一般的なコード・パスのコストを削減できる
サンプル
volatile と synchronized を組み合わせて「安価な読み書きロック」を構成する

synchronizedでインクリメント演算がアトミックなことを保証されたスレッド・セーフなカウンタである
同カウンタに volatile を使うことで現在の結果の可視性を保証できる

更新頻度が低ければ
読み取りパスのオーバーヘッドが
volatile の読み取り(競合のないロック取得よりも一般的にはコストが低い) のみ
であるため パフォーマンス向上になる

|@ThreadSafe
|public class CheesyCounter {
| // Employs the cheap read-write lock trick
| // All mutative operations MUST be done with the 'this' lock held
| @GuardedBy("this") private volatile int value;

| public int getValue() { return value; }

| public synchronized int increment() {
|  return value++;
| }
|}


「安価な読み書きロック」と呼ばれるのは
読み取りと書き込みの同期機構が異なるため
書き込みは
volatile を使うための最初の条件に違反しているため
volatile で安全にカウンタを実装できず
ロックを使う必要がある
しかし
読み取りは
volatileで現在の値の可視性を保証できる
結果
変更される可能性がある操作にはロックを使い
読み取りのみの操作には volatile を使う

ロックでは
一度にある値にアクセスできるのは 1 つのスレッドのみで
volatile の読み取りは複数のスレッドからのアクセスを許可する

ロックより volatile の方がを共有の度合いを高められる



ref:
Javaの理論と実践: volatile を扱う, 2007/6/19
http://www.ibm.com/developerworks/jp/java/library/j-jtp06197.html

tag : Java volatile

2009-11-23 23:25 : 開発 : コメント : 0 : トラックバック : 0 :
コメントの投稿
非公開コメント

« next  ホーム  prev »

search

ad



counter


tag cloud

category cloud