複合操作(compound operation)とは、2つ以上の独立した操作から構成される操作を意味する。前置インクリメントまたは後置インクリメント演算子(++)、前置デクリメントまたは後置デクリメント演算子(--)、複合代入演算子(compound assignment operator)を含む式は必ず、複合操作となる。複合代入式では、*=、/=、%=、+=、-=、<<=、>>=、>>>=、^=、|= といった演算子を使用する[JLS 2005]。データ競合や競合状態を防ぐために、共有変数に対する複合操作はアトミックに行わなければならない。
スレッドセーフなクラスに属するアトミックなメソッドをまとめて呼び出した場合のアトミック性については、「VNA03-J. アトミックなメソッドをまとめた呼び出しがアトミックであると仮定しない」を参照。
64ビット値の読み書きはアトミックでなくてもよいことがJava言語仕様では規定されている。詳細は「VNA05-J. 64ビット値の読み書きはアトミックに行う」を参照。
違反コード (論理否定)
以下の違反コード例では、boolean 型の共有変数 flag を宣言し、現在の flag の値を反転する toggle() メソッドを実装している。
final class Flag {
private boolean flag = true;
public void toggle() { // スレッドセーフではないメソッド
flag = !flag;
}
public boolean getFlag() { // スレッドセーフではないメソッド
return flag;
}
}
flag の値は、読み取られた後で反転され、書き戻される。そのため、このコードを実行するとデータ競合が発生するかもしれない。
toggle() メソッドを呼び出す2つのスレッドを例に考えてみよう。flag を2回反転すれば、元の値に戻るのが期待される動作である。しかし、以下に示すシナリオでは、flag が期待とは異なる値になる。
| 時間 | flagの値 | スレッド | 動作 |
|---|---|---|---|
| 1 | true | t1 | flag の現在値 true を一時変数に読み取る |
| 2 | true | t2 | flag の現在値 true (変化なし)を一時変数に読み取る |
| 3 | true | t1 | 一時変数を false に切り替える |
| 4 | true | t2 | 一時変数を false に切り替える |
| 5 | false | t1 | 一時変数の値を flag に書き込む |
| 6 | false | t2 | 一時変数の値を flag に書き込む |
t2 の呼び出し結果は flag の値に反映されず、あたかも toggle() が1回だけ呼びされたかのようにプログラムは動作する。
違反コード (ビット単位反転)
同様に、toggle()メソッドは、現在の flag の値を反転するために、複合代入演算子 ^= を使用する場合もあるだろう。
final class Flag {
private boolean flag = true;
public void toggle() { // スレッドセーフではないメソッド
flag ^= true; // flag = !flag; と同じ結果となる
}
public boolean getFlag() { // スレッドセーフではないメソッド
return flag;
}
}
上記のコードもスレッドセーフではない。^= はアトミックではない複合操作であり、データ競合が発生する。
違反コード (volatile 変数)
flag を volatile 宣言しても、スレッドセーフにはならない。
final class Flag {
private volatile boolean flag = true;
public void toggle() { // スレッドセーフではないメソッド
flag ^= true;
}
public boolean getFlag() { // スレッドセーフなメソッド
return flag;
}
}
変数を volatile 宣言しても、その変数への複合操作はアトミックには行われない。それゆえ、このコードはスレッドセーフではない。
適合コード (メソッドの同期)
以下の適合コードでは、toggle() メソッドと getFlag() メソッドの両方を synchronized 修飾している。
final class Flag {
private boolean flag = true;
public synchronized void toggle() {
flag ^= true; // flag = !flag; と同じ
}
public synchronized boolean getFlag() {
return flag;
}
}
このコードでは this インスタンスの固有ロックを使用することにより flag フィールドに対する読み書きを保護している。また、メソッドを synchronized 修飾することで、flag の変更が全スレッドから見えることを保証している。スレッドの考えられる実行順序は2通りのみである。以下にその1つを示す。
| 時間 | flagの値 | スレッド | 動作 |
|---|---|---|---|
| 1 | true | t1 | flag の現在値 true を一時変数に読み取る |
| 2 | true | t1 | 一時変数を false に切り替える |
| 3 | false | t1 | 一時変数の値を flag に書込む |
| 4 | false | t2 | flag の現在値 false を一時変数に読み取る |
| 5 | false | t2 | 一時変数を true に切り替える |
| 6 | true | t2 | 一時変数の値を flag に書込む |
もうひとつの実行順序でも同じ操作を行うが、t2 スレッドの処理は t1 の前に開始し、終了する。また、「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」を順守することで、信頼できない呼出し側がロックオブジェクトにアクセスできなくなり、ロックオブジェクトが誤用される可能性を低減できる。
適合コード (volatile変数の読取り、同期書込み)
以下の適合コードでは、getFlag() メソッドは synchronized 修飾されておらず、flag は volatile 宣言されている。getFlag()メソッドにおける flag の読取りはアトミックに行われ、volatile 修飾により flag の可視性が確保されているので、このルールはルールに適合している。toggle() メソッドは依然としてアトミックでない操作を行うため、synchronized 修飾しておく必要がある。
final class Flag {
private volatile boolean flag = true;
public synchronized void toggle() {
flag ^= true; // flag = !flag と同じ
}
public boolean getFlag() {
return flag;
}
}
ゲッターメソッドで、volatile 宣言されたフィールドの値を返す以外の処理を同期せずに行う場合は、この手法を用いてはならない。また、読取りのパフォーマンスが特に重要でない限り、通常のメソッド同期ではなくこの手法を用いる優位性はあまりないであろう[Goetz 2006a]。
適合コード (リードライトロック)
以下の適合コードでは、アトミック性と可視性を確保するために、リードライトロックを使用している。
final class Flag {
private boolean flag = true;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void toggle() {
writeLock.lock();
try {
flag ^= true; // flag = !flag と同じ
} finally {
writeLock.unlock();
}
}
public boolean getFlag() {
readLock.lock();
try {
return flag;
} finally {
readLock.unlock();
}
}
}
リードライトロックは、共有状態が複数のリーダーあるいは単一のライターからアクセスされるのを許すが、読み取りと書き込みを同時に行うことは許さない。Goetz は次のように述べている [Goetz 2006a] 。
リードライトロックは、マルチプロセッサシステムにおいて、ほとんど読取りしか行われないデータ構造に頻繁にアクセスする場合には性能の向上につながるが、その他の条件では、その複雑さゆえに排他的ロックよりも性能が劣ることがある。
アプリケーションのプロファイリングを行うことで、リードライトロックが適しているかどうかを判断できる。
適合コード (AtomicBooleanクラス)
以下の適合コードでは、flag を AtomicBoolean 型として宣言している。
import java.util.concurrent.atomic.AtomicBoolean;
final class Flag {
private AtomicBoolean flag = new AtomicBoolean(true);
public void toggle() {
boolean temp;
do {
temp = flag.get();
} while (!flag.compareAndSet(temp, !temp));
}
public AtomicBoolean getFlag() {
return flag;
}
}
flag の更新は、AtomicBoolean クラスの compareAndSet() メソッドを使って行われる。すべての更新は、他のスレッドから見える。
違反コード (プリミティブ型変数の加算)
以下の違反コード例では、複数のスレッドが setValues() メソッドを呼び出し、フィールド a および b に値を代入することができる。このクラスでは整数オーバーフローの発生をチェックしていないため、Adder クラスの利用者は、加算した結果がオーバーフローしないような値を setValues() メソッドの引数に指定する必要がある。
final class Adder {
private int a;
private int b;
public int getSum() {
return a + b;
}
public void setValues(int a, int b) {
this.a = a;
this.b = b;
}
}
getSum() メソッドには競合状態が存在する。たとえば、変数 a、b の値がそれぞれ 0、Integer.MAX_VALUE である状態で、スレッド1が getSum() メソッドを呼び出すのと同時にスレッド2が setValues(Integer.MAX_VALUE, 0) を呼び出す場合、getSum() メソッドは、0 または Integer.MAX_VALUE を返すか、あるいは整数オーバーフローを発生させる。スレッド2が変数 a に Integer.MAX_VALUEを代入した後であり、かつ変数 b に 0 をセットする前に、スレッド1が変数 a と b の値を読み取ると、オーバーフローが発生する。
これらの複合操作(compound operation)では複数の変数に対する読み書きを行っており、変数を volatile 宣言したとしても、問題は解決しない。
違反コード (AtomicIntegerの加算)
以下の違反コード例では、変数 a、b をアトミックに操作可能な AtomicInteger に置き換えている。
final class Adder {
private final AtomicInteger a = new AtomicInteger();
private final AtomicInteger b = new AtomicInteger();
public int getSum() {
return a.get() + b.get();
}
public void setValues(int a, int b) {
this.a.set(a);
this.b.set(b);
}
}
2つの int フィールドを単に AtomicInteger へ置き換えただけでは、競合状態は解消しない。複合操作である a.get() + b.get() はアトミックに行われないからである。
適合コード (アトミックな加算)
以下の適合コードでは、setValues() メソッドと getSum() メソッドを synchronized 修飾することで、アトミック性を確保している。
final class Adder {
private int a;
private int b;
public synchronized int getSum() {
// オーバーフローの検査をここで行う
return a + b;
}
public synchronized void setValues(int a, int b) {
this.a = a;
this.b = b;
}
}
synchronized 修飾したこれらのメソッドの中で行われる処理は、同じオブジェクトの固有ロックを使用して同期する他のメソッドに対し、アトミックに実行される。したがって、たとえば getSum() メソッドに整数オーバーフローのチェックを追加しても競合状態は発生しない。
リスク評価
共有変数に対する操作がアトミックに行われない場合、予期せぬ結果が生じる可能性がある。たとえば、他のユーザに関する情報が取得され、情報漏えいにつながる恐れがある。
| ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
|---|---|---|---|---|---|
| VNA02-J | 中 | 中 | 中 | P8 | L2 |
自動検出
並行処理によって共有される値の更新がアトミックに行われないケースを検知できる静的解析ツールは存在する。値の更新がアトミックに行われない場合、その更新結果はスレッドの実行順序に応じて変わる。これらの静的解析ツールは、スレッド間で共有されるデータがロックを取得されずにアクセスされ、競合状態を発生させ得るケースについて、検知することができる。
関連ガイドライン
| MITRE CWE | CWE-667. Improper locking |
| CWE-413. Improper resource locking | |
| CWE-366. Race condition within a thread | |
| CWE-567. Unsynchronized access to shared data in a multithreaded context |
参考文献
| [API 2006] | Class AtomicInteger |
| [Bloch 2008] | Item 66. Synchronize access to shared mutable data |
| [Goetz 2006a] | 2.3, Locking |
| [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 | |
| [Lea 2000] | Section 2.2.7, The Java Memory Model |
| Section 2.1.1.1, Objects and Locks | |
| [Tutorials 2008] | Java Concurrency Tutorial |
翻訳元
これは以下のページを翻訳したものです。
VNA02-J. Ensure that compound operations on shared variables are atomic (revision 223)
