あるスレッドで共有プリミティブ型変数を読み取ったとき、その値に他のスレッドによる最新の書込みが反映されているとは限らない。つまりそのスレッドは、共有変数の最新ではない値を得る可能性がある。最新の更新が反映された値を確実に得るためには、変数を volatile 宣言するか、変数に対する読み書きを同期する必要がある。
共有変数を volatile 宣言することでスレッドセーフに可視性が保証できるのは、以下に示す2つの条件をどちらも満たす場合のみである。
- ある変数への書込みが、現在の(書込み前の)値に依存しない。
- ある変数への書込みが、他の変数の読取りと書込みを伴うアトミックではない複合操作の結果に依存しない。(詳細は「VNA02-J. 共有変数への複合操作のアトミック性を確保する」を参照。)
単一のスレッドだけが変数の値を更新することが確実な場合、第一の条件は満たさなくてもよい[Goetz 2006a]。しかし、スレッドが1つだけしか存在しないことを前提にしたコードは、エラーを引き起こしやすく、保守が困難になる。したがって、単一のスレッドに依存した設計は、本ルールでは認められるが、一般には推奨されない。
同期を行うほうが、単純に変数を volatile 宣言するよりもプログラムの動作を予測しやすく、かつ多くの場合より安全なコードになる。しかし、過度に同期を行うと、パフォーマンス上のオーバーヘッドは大きくなり、スレッド間の競合やデッドロックを引き起こすことになる。
変数の volatile 宣言やコードの正しい同期を行うことで、64ビットプリミティブ型変数(longとdouble)へのアクセスがアトミックに行われることを保証できる(これらの変数を複数のスレッドで共有する場合の詳細は「VNA05-J. 64ビット値の読み書きはアトミックに行う」を参照)。
違反コード (volatile 宣言されていないフラグ)
以下の違反コード例では、run() メソッドの中で volatile 宣言されていな変数 done をチェックしている。この変数は shutdown() メソッドで true にセットされる。
final class ControlledStop implements Runnable { private boolean done = false; @Override public void run() { while (!done) { try { // ... Thread.currentThread().sleep(1000); // 何らかの処理を行う } catch(InterruptedException ie) { Thread.currentThread().interrupt(); // 割込みステータスをリセット } } } public void shutdown() { done = true; } }
一方のスレッドが shutdown() メソッドを呼び出して変数をセットしても、他方のスレッドからはその変更が見えないかもしれない。その結果、他方のスレッドは done 変数がまだ false のままであると判断して、sleep()メソッドを誤って呼び出す恐れがある。コンパイラや実行時コンパイラ(JIT)は、done 変数の値が同一スレッド内で変更されることがないと判断すると、コードを最適化することが許されている。このコードでは、最適化によって無限ループが発生する可能性がある。
適合コード (volatile変数)
以下の適合コードでは、done 変数を volatile 宣言し、done 変数への書込みが他のスレッドから見えるようにしている。
final class ControlledStop implements Runnable { private volatile boolean done = false; @Override public void run() { while (!done) { try { // ... Thread.currentThread().sleep(1000); // 何らかの処理を行う } catch(InterruptedException ie) { Thread.currentThread().interrupt(); // 割込みステータスをリセット } } } public void shutdown() { done = true; } }
適合コード (AtomicBooleanクラス)
以下の適合コードでは、done 変数は java.util.concurrent.atomic.AtomicBoolean 型として宣言されている。アトミック型に宣言することで、書込みが他のスレッドから見えることを保証する。
final class ControlledStop implements Runnable { private final AtomicBoolean done = new AtomicBoolean(false); @Override public void run() { while (!done.get()) { try { // ... Thread.currentThread().sleep(1000); // 何らかの処理を行う } catch(InterruptedException ie) { Thread.currentThread().interrupt(); // 割込みステータスをリセット } } } public void shutdown() { done.set(true); } }
適合コード (synchronizedメソッド)
以下の適合コードでは、Class オブジェクトの固有ロック(intrinsic lock)を使用することで、変数の更新が他のスレッドからも見える。
final class ControlledStop implements Runnable { private boolean done = false; @Override public void run() { while (!isDone()) { try { // ... Thread.currentThread().sleep(1000); // 何らかの処理を行う } catch(InterruptedException ie) { Thread.currentThread().interrupt(); // 割込みステータスをリセット } } } public synchronized boolean isDone() { return done; } public synchronized void shutdown() { done = true; } }
上記のコードは確かに本ルールに適合してはいるが、固有ロックの使用によりスレッド間のやり取りがブロックされ、ロックの競合が発生するかもしれない。一方、volatile 共有変数を使用すれば、スレッド間のやり取りはブロックされない。また、過度の同期はデッドロックにつながりやすい。
変数の更新後の値が変更前の値に依存する場合のように volatile キーワードや java.util.concurrent.atomic.Atomic* フィールドを使えない場合、同期を行うほうが安全である。詳細は「VNA02-J. 共有変数への複合操作のアトミック性を確保する」を参照。
「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」を順守することで、信頼できない呼出し側がロックオブジェクトにアクセスできなくなり、ロックオブジェクトが誤用される可能性を低減することができる。
例外
VNA00-EX1: Class オブジェクト(java.lang.Class)は、仮想マシンによって生成され、使用される前に必ず初期化される。それゆえ、Class オブジェクトのスレッド間の可視性は最初から保証されている。
リスク評価
共有プリミティブ型変数の可視性を確保できない場合、スレッドが変数の最新でない値を参照する恐れがある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
VNA00-J | 中 | 中 | 中 | P8 | L2 |
自動検出
このルールの違反を検知することができる静的解析ツールが存在する。
関連ガイドライン
MITRE CWE | CWE-667. Improper locking |
CWE-413. Improper resource locking | |
CWE-567. Unsynchronized access to shared data in a multithreaded context |
参考文献
[Bloch 2008] | Item 66. Synchronize access to shared mutable data |
[Goetz 2006a] | 3.4.2, Example: Using Volatile to Publish Immutable Objects |
[JLS 2005] | Chapter 17, Threads and Locks |
§17.4.5, Happens-Before Order | |
§17.4.3, Programs and Program Order | |
§17.4.8, Executions and Causality Requirements | |
[JPL 2006] | 14.10.3, The Happens-Before Relationship |
翻訳元
これは以下のページを翻訳したものです。
VNA00-J. Ensure visibility when accessing shared primitive variables (revision 176)