JavaRush /Java Blog /Random-JA /ボラティリティの管理
lexmirnov
レベル 29
Москва

ボラティリティの管理

Random-JA グループに公開済み

揮発性変数の使用に関するガイドライン

ブライアン・ゲッツ著 2007 年 6 月 19 日 原文: Managing Volatility Java の Volatile 変数は「同期光」と呼ばれます。同期ブロックよりも使用するコードが少なく、多くの場合高速に実行されますが、実行できるのは同期ブロックの一部だけです。この記事では、volatile を効果的に使用するためのいくつかのパターンと、volatile を使用しない場合についてのいくつかの警告を紹介します。ロックには、相互排他 (ミューテックス) と可視性という 2 つの主な機能があります。相互排他とは、ロックを一度に 1 つのスレッドのみが保持できることを意味し、このプロパティを使用して共有リソースのアクセス制御プロトコルを実装し、一度に 1 つのスレッドのみが共有リソースを使用できるようにすることができます。可視性はより微妙な問題であり、その目的は、ロックが解放される前にパブリック リソースに加えられた変更が、そのロックを引き継ぐ次のスレッドに確実に表示されるようにすることです。同期によって可視性が保証されない場合、スレッドはパブリック変数の古い値または不正な値を受け取る可能性があり、これにより多くの深刻な問題が発生する可能性があります。
揮発性変数
揮発性変数は同期変数の可視性プロパティを持っていますが、原子性がありません。これは、スレッドが揮発性変数の最新の値を自動的に使用することを意味します。これらはスレッド セーフのために使用できますが非常に限られたケースで使用できます。つまり、複数の変数間、または変数の現在値と将来の値の間に関係が導入されない場合です。したがって、volatile だけでは、カウンター、ミューテックス、または不変部分が複数の変数 (たとえば、「start <=end」) に関連付けられているクラスを実装するには十分ではありません。揮発性ロックは、シンプルさまたはスケーラビリティという 2 つの主な理由のいずれかで選択できます。一部の言語構造は、ロックの代わりに揮発性変数を使用すると、プログラム コードとして記述しやすく、後で読んで理解するのが容易になります。また、ロックとは異なり、スレッドをブロックできないため、スケーラビリティの問題が発生しにくくなります。書き込みよりも読み取りの方がはるかに多い状況では、揮発性変数の方がロックよりもパフォーマンス上の利点が得られます。
volatile を正しく使用するための条件
限られた状況においては、ロックを揮発性ロックに置き換えることができます。スレッドセーフであるためには、次の両方の基準を満たす必要があります。
  1. 変数に書き込まれる内容は、その現在の値とは無関係です。
  2. この変数は、他の変数の不変条件に関与しません。
簡単に言えば、これらの条件は、揮発性変数に書き込むことができる有効な値が、変数の現在の状態を含むプログラムの他の状態から独立していることを意味します。最初の条件は、スレッドセーフなカウンターとしての揮発性変数の使用を除外します。インクリメント (x++) は単一の操作のように見えますが、実際には、アトミックに実行する必要がある一連の読み取り、変更、書き込み操作全体であり、volatile では提供されません。有効な操作では、x の値が操作全体を通じて同じであることが必要ですが、volatile を使用するとこれを実現できません。(ただし、値が 1 つのスレッドのみから書き込まれることが保証できる場合は、最初の条件を省略できます。) ほとんどの状況では、最初の条件または 2 番目の条件のいずれかに違反するため、揮発性変数は、スレッドの安全性を実現するためのアプローチとして、同期変数に比べてあまり一般的に使用されません。リスト 1 は、さまざまな数値を含む非スレッドセーフ クラスを示しています。これには不変式が含まれています。下限は常に上限以下です。 @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; } } このように範囲状態変数が制限されているため、クラスがスレッド セーフであることを保証するには、下位フィールドと上位フィールドを揮発性にするだけでは十分ではありません。同期は引き続き必要です。そうしないと、遅かれ早かれ不運になり、2 つのスレッドが不適切な値で setLower() と setUpper() を実行すると、範囲が不整合な状態になる可能性があります。たとえば、初期値が (0, 5) の場合、スレッド A が setLower(4) を呼び出し、同時にスレッド B が setUpper(3) を呼び出します。これらのインターリーブ操作は両方ともチェックに合格しますが、エラーになります。それは不変式を保護することになっています。その結果、範囲は (4, 3) - 不正な値になります。setLower() と setUpper() を他の範囲操作に対してアトミックにする必要がありますが、フィールドを volatile にしてもそれはできません。
パフォーマンスに関する考慮事項
volatile を使用する最初の理由は、単純さです。状況によっては、そのような変数を使用する方が、それに関連付けられたロックを使用するよりも単純に簡単です。2 番目の理由はパフォーマンスです。場合によっては、揮発性の方がロックよりも高速に動作することがあります。特に Java 仮想マシンの内部操作に関しては、「X は常に Y よりも高速である」のような、正確で包括的なステートメントを作成することは非常に困難です。(たとえば、状況によっては JVM がロックを完全に解放する可能性があるため、抽象的な方法で揮発性と同期のコストを議論することが困難になります)。ただし、最新のプロセッサ アーキテクチャのほとんどでは、揮発性変数の読み取りコストは、通常の変数の読み取りコストとそれほど変わりません。可視性のためにメモリフェンシングが必要なため、揮発性変数の書き込みのコストは通常​​の変数の書き込みよりも大幅に高くなりますが、一般にロックを設定するよりも安価です。
volatile を適切に使用するためのパターン
多くの並行性専門家は、揮発性変数はロックよりも正しく使用するのが難しいため、それらの使用を完全に避ける傾向があります。ただし、注意深く従えば、さまざまな状況で安全に使用できる明確に定義されたパターンがいくつかあります。volatile の制限を常に尊重してください。プログラム内の他のものから独立した volatile のみを使用してください。そうすることで、これらのパターンで危険な領域に陥ることを防ぐことができます。
パターン #1: ステータス フラグ
おそらく、可変変数の標準的な使用法は、初期化の完了やシャットダウン要求など、重要な 1 回限りのライフサイクル イベントが発生したことを示す単純なブール ステータス フラグです。多くのアプリケーションには、リスト 2 に示すように、「シャットダウンの準備ができるまで実行を続ける」という形式の制御構造が含まれています。 shutdown() メソッドは、ループの外側のどこか (別のスレッド上) から呼び出される可能性があります volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } 。したがって、変数の正しい可視性を確保するには同期が必要です。(JMX リスナー、GUI イベント スレッドのアクション リスナー、RMI 経由、Web サービス経由などから呼び出すことができます)。ただし、同期されたブロックを含むループは、リスト 2 のように volatile 状態フラグを含むループよりもはるかに扱いにくくなります。volatile を使用するとコードの記述が容易になり、状態フラグは他のプログラムの状態に依存しないため、これは一例です。揮発性物質を上手に活用しましょう。このようなステータス フラグの特徴は、通常、状態遷移が 1 つだけであることです。shutdownRequested フラグが false から true に変化すると、プログラムがシャットダウンします。このパターンは、前後に変化する状態フラグに拡張できますが、これは遷移サイクル (偽から真、偽へ) が外部介入なしで発生する場合に限られます。それ以外の場合は、アトミック変数など、ある種のアトミック遷移メカニズムが必要です。
パターン #2: 1 回限りの安全な公開
同期がない場合に発生する可能性のある可視性エラーは、プリミティブ値の代わりにオブジェクト参照を記述する場合にさらに困難な問題になる可能性があります。同期を行わないと、別のスレッドによって書き込まれたオブジェクト参照の現在の値を確認できますが、そのオブジェクトの古い状態の値も確認できます。(この脅威は、悪名高い二重チェック ロックの問題の根本にあります。二重チェック ロックでは、オブジェクト参照が同期せずに読み取られるため、実際の参照は見えても、部分的に構築されたオブジェクトがそこから取得される危険があります。) object は、揮発性オブジェクトへの参照を作成することです。リスト 3 は、起動時にバックグラウンド スレッドがデータベースからデータをロードする例を示しています。このデータを使用しようとする他のコードは、使用する前にデータが公開されているかどうかを確認します。 public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } theFlooble への参照が揮発性でない場合、doWork() 内のコードは、theFlooble を参照しようとすると、部分的に構築された Flooble が表示される危険性があります。このパターンの重要な要件は、公開されたオブジェクトがスレッドセーフであるか、実質的に不変である必要があることです (実質的に不変とは、公開後にその状態が決して変わらないことを意味します)。Volatile リンクを使用すると、オブジェクトが公開された形式で確実に表示されるようになりますが、公開後にオブジェクトの状態が変化した場合は、追加の同期が必要になります。
パターン #3: 独立した観察
volatile の安全な使用のもう 1 つの簡単な例は、プログラム内で使用するために観察結果が定期的に「公開」される場合です。たとえば、現在の温度を検出する環境センサーがあります。バックグラウンド スレッドは、数秒ごとにこのセンサーを読み取り、現在の温度を含む揮発性変数を更新できます。他のスレッドは、変数内の値が常に最新であることを認識して、この変数を読み取ることができます。このパターンのもう 1 つの用途は、プログラムに関する統計を収集することです。リスト 4 は、認証メカニズムが最後にログインしたユーザーの名前をどのように記憶できるかを示しています。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; } } このパターンは前のパターンを拡張したものです。値はプログラム内の他の場所で使用するために公開されますが、その公開は 1 回限りのイベントではなく、一連の独立したイベントです。このパターンでは、公開された値が事実上不変であること、つまり公開後にその状態が変わらないことが必要です。値を使用するコードでは、値がいつでも変更される可能性があることを認識する必要があります。
パターン #4: 「揮発性 Bean」パターン
「揮発性 Bean」パターンは、JavaBeans を「美化された構造体」として使用するフレームワークに適用できます。「揮発性 Bean」パターンは、ゲッターやセッターを備えた独立したプロパティのグループのコンテナとして JavaBean を使用します。「揮発性 Bean」パターンの理論的根拠は、多くのフレームワークは変更可能なデータ ホルダー (HttpSession など) 用のコンテナーを提供しますが、これらのコンテナーに配置されるオブジェクトはスレッドセーフである必要があるということです。volatile Bean パターンでは、すべての JavaBean データ要素は揮発性であり、ゲッターとセッターは自明である必要があります。これらには、対応するプロパティの取得または設定以外のロジックが含まれるべきではありません。さらに、オブジェクト参照であるデータ メンバーの場合、そのオブジェクトは実質的に不変でなければなりません。(配列参照が volatile と宣言されると、要素自体ではなくその参照のみが volatile プロパティを持つため、配列参照フィールドは許可されません。) 他の volatile 変数と同様に、JavaBeans のプロパティに関連付けられた不変条件や制限は存在できません。 。「揮発性 Bean」パターンを使用して作成された JavaBean の例をリスト 5 に示します。 @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 つの操作 (読み取り、追加、保存) を削減するものであるため、問題が発生した場合、複数のスレッドが同時に揮発性カウンターをインクリメントしようとすると、更新された値が失われます。ただし、変更よりも読み取りが大幅に多い場合は、組み込みロックと揮発性変数を組み合わせて、コード パス全体のオーバーヘッドを削減できます。リスト 6 は、インクリメント操作がアトミックであることを保証するために synchronized を使用し、現在の結果が確実に表示されることを保証するために 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 を使用して読み取りを保護すると、すべてのコードにロックを使用する場合よりも高いレベルの交換が得られます。読み取りと記録。ただし、このパターンの脆弱性に注意してください。2 つの競合する同期メカニズムがあるため、このパターンの最も基本的なアプリケーションを超えた場合、非常に複雑になる可能性があります。
まとめ
揮発性変数は、ロックよりも単純ですが弱い同期形式であり、場合によっては、組み込みロックよりもパフォーマンスやスケーラビリティが向上します。volatile を安全に使用するための条件 (変数が他の変数および自身の以前の値の両方から完全に独立している) を満たしている場合は、synchronized を volatile に置き換えることでコードを簡素化できる場合があります。ただし、volatile を使用するコードは、ロックを使用するコードよりも脆弱であることがよくあります。ここで提案するパターンは、ボラティリティが同期の合理的な代替手段である最も一般的なケースをカバーしています。これらのパターンに従い、その制限を超えないよう注意することで、メリットが得られる場合でも volatile を安全に使用できます。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION