同期プリミティブの誤用は、並行処理に関する問題を生み出す大きな原因の一つである。再利用されるオブジェクトを使って同期を行うと、デッドロックや予期せぬ動作につながる場合がある。したがって、再利用されるオブジェクトを使って同期を行ってはならない。
違反コード (Boolean ロックオブジェクト)
以下の違反コード例では、Boolean クラスのロックオブジェクトを使って同期を行っている。
private final Boolean initialized = Boolean.FALSE; public void doSomething() { synchronized (initialized) { // ... } }
Boolean は true と false の2つの値しかとらないため、ロックとしての使用には適さない。同じ値を持つすべての Boolean リテラルは、JVM環境下でただ1つの Boolean クラスのインスタンスを共有する。前述のコード例において、initialized は Boolean.FALSE 値に対応するインスタンスを参照している。他のコードが不用意に Boolean リテラルの Boolean.FALSE 値を使って同期を行った場合、ロックインスタンスが再利用され、システムは応答しなくなるかデッドロックに陥る可能性がある。
違反コード (プリミティブ型変数のボクシング)
以下の違反コード例では、ボクシングした Integer オブジェクトをロックとして使用している。
int lock = 0; private final Integer Lock = lock; // ボクシングされた Lock は共有される public void doSomething() { synchronized (Lock) { // ... } }
プリミティブ型変数をボクシングしたオブジェクトは、一定の範囲の整数値では同じインスタンスが使われる場合があるため、Boolean クラスと同様の理由でロックオブジェクトとして使用するには問題がある。プリミティブ型変数の値が1バイトで表現可能な場合、その値を持つラッパーオブジェクトのインスタンスは再利用される。それ以外の値に対してもラッパーオブジェクトを再利用することが JVM 実装には許されている。ボクシングした Integer ラッパーオブジェクトの固有ロックは安全ではないが、new オペレータを使って (new Integer(value)) 構築した Integer のオブジェクトインスタンスは一意であり、再利用されない。一般に、データ型に関係なく、ボクシングした値を含むオブジェクトによるロックは安全ではない。
適合コード (Integerロックオブジェクト)
以下の適合コードでは、プライベートロックオブジェクト作法の亜種である、ボクシングしていない Integer をロックしている。doSomething() メソッドでは、Integer のインスタンスである Lock の固有ロックを使用して同期している。
int lock = 0; private final Integer Lock = new Integer(lock); public void doSomething() { synchronized (Lock) { // ... } }
インスタンスを明示的に構築した場合、Integer オブジェクトは一意の参照を持ち、その固有ロックは、他の Integer オブジェクトや同じ値を保持するボクシングされた整数と共有されることはない。このコードはルールに適合してはいるが、ボクシングされた整数をロックオブジェクトとして使ってもよいという間違った認識をプログラマに与えるかもしれず、コードの保守上問題になる恐れがある。より適切な解決方法は、このルールの最後の適合コードで紹介する、private final 宣言したロックオブジェクトを使った同期である。
違反コード (internしたStringオブジェクト)
以下の違反コード例では、intern した String オブジェクトをロックに使用している。
private final String lock = new String("LOCK").intern(); public void doSomething() { synchronized (lock) { // ... } }
Java API仕様は java.lang.String クラスについて以下のように記述している[API 2006]。
intern() メソッドが呼び出されたときに、equals(Object) メソッドによってこの String オブジェクトに等しいと判定される文字列がプールにすでに存在した場合は、プール内の該当する文字列が返される。そうでない場合は、この String オブジェクトがプールに追加され、この String オブジェクトへの参照が返される。
したがって、intern された String オブジェクトは、JVM においてはグローバル変数のように振る舞う。この違反コード例のように、クラスのすべてのインスタンスが自身の lock フィールドを保持していたとしても、それらのインスタンスのフィールドはすべて、共通の String 定数を参照している。String 定数によるロックにも、Boolean 定数によるロックと同じ再利用の問題が存在する。
さらに、クラスが他パッケージの信頼できないコードからアクセス可能である場合、この脆弱性が悪用される可能性がある(詳細は「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」を参照)。
違反コード (Stringリテラル)
以下の違反コード例では、final 宣言された String リテラルをロックに使用している。
// このバグは jetty-6.1.3 BoundedThreadPool に見つかった private final String lock = "LOCK"; public void doSomething() { synchronized (lock) { // ... } }
String リテラルは定数であり、自動的に intern される(プール内で共有される)。したがって、このコードは前述の違反コード例と同じ問題を抱えている。
適合コード (Stringインスタンス)
以下の適合コードでは、intern されていない String インスタンスをロックに使用している。
private final String lock = new String("LOCK"); public void doSomething() { synchronized (lock) { // ... } }
String インスタンスは String リテラルとは異なる。String インスタンスは一意の参照を持ち、他の String インスタンスやリテラルとは共有されないそれ自身の固有ロックを持つ。よりよいアプローチは、次に示す適合コードのように、private final 宣言されたロックオブジェクトを使って同期することである。
適合コード (private final宣言されたObjectクラスのロックオブジェクト)
以下の適合コードでは、private final 宣言されたロックオブジェクトを使って同期している。この方法は、java.lang.Object のインスタンスが有用なケースの一つである。
private final Object lock = new Object(); public void doSomething() { synchronized (lock) { // ... } }
Object クラスのオブジェクトをロックとして使用することの詳細は「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」を参照。
リスク評価
並行処理に関する脆弱性の多くは、不適切なロックオブジェクトを使用することから発生する。ロックオブジェクトの特性を十分考慮して選ぶことが重要である。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
LCK01-J | 中 | 中 | 中 | P8 | L2 |
自動検出
このルールの違反を検出できる静的解析ツールは存在する。
参考文献
[API 2006] | Class String, Collections |
[Findbugs 2008] | |
[Miller 2009] | Locking |
[Pugh 2008] | Synchronization |
[Tutorials 2008] | Wrapper Implementations |
翻訳元
これは以下のページを翻訳したものです。
LCK01-J. Do not synchronize on objects that may be reused (revision 153)