例外が発生するとロックの解放が行われなくなり、デッドロックが発生する可能性がある。Java API [API 2006] には以下のように記載されている。
ReentrantLock は、最後にロックに成功したがまだロック解放していないスレッドにより「所有」される。ロックが別のスレッドに所有されていない場合、ロックを呼び出すスレッドが復帰してロックの取得に成功する。
つまり、解放されていないロックを他のスレッドが取得することはできないということである。例外が発生したら、プログラムは所有しているすべてのロックを解放しなければいけない。一方、メソッド同期およびブロック同期で使用されている固有ロックは、スレッドの異常終了のような例外発生時には自動的に解放される。
違反コード (チェック例外)
以下の違反コード例では、ReentrantLock を使用してリソースを保護しているが、ファイルの操作中に例外が発生した場合にロックを解放していない。例外がスローされると、実行文の制御は catch ブロックに移り、unlock() メソッドは実行されない。
public final class Client { public void doSomething(File file) { final Lock lock = new ReentrantLock(); InputStream in = null; try { lock.lock(); in = new FileInputStream(file); // オープンされたファイルに関する操作 lock.unlock(); } catch (FileNotFoundException x) { // 例外の処理 } finally { if (in != null) { try { in.close(); } catch (IOException x) { // 例外の処理 } } } } }
適合コード (finallyブロック)
以下の適合コードでは、例外をスローする可能性のある操作を、ロックを獲得した直後の try ブロックで行っている。try ブロックの直前でロックを獲得しているため、finally ブロックの実行時にはロックが確実に保持されていることになる。finally ブロック内で Lock.unlock() メソッドを呼び出すことで、例外の発生に関係なく、ロックは確実に解放される。
public final class Client { public void doSomething(File file) { final Lock lock = new ReentrantLock(); InputStream in = null; lock.lock(); try { in = new FileInputStream(file); // オープンされたファイルに関する操作 } catch (FileNotFoundException fnf) { // ハンドラへ処理を移す } finally { lock.unlock(); if (in != null) { try { in.close(); } catch (IOException e) { // ハンドラへ処理を移す } } } } }
適合コード (Execute-Around手法)
Execute-Around 手法は、リソース割当てとクリーンアップ操作を行うための一般的な仕組みを提供する。クライアントプログラムでは必要な機能の実装のみに集中することができる。この手法は、ソースコードを分かりやすくするとともに、リソース管理を安全に行うための仕組みを提供する。
以下の適合コードにおいて、クラス Client の doSomething() メソッドは、ロックの獲得と解放や、ファイルのオープンとクローズ操作を実装せずに、必要な機能だけを LockAction インタフェースの doSomethingWithFile() メソッドとして実装している。リソース管理のための動作はすべて ReentrantLockAction クラスにカプセル化されている。
public interface LockAction { void doSomethingWithFile(InputStream in); } public final class ReentrantLockAction { public static void doSomething(File file, LockAction action) { Lock lock = new ReentrantLock(); InputStream in = null; lock.lock(); try { in = new FileInputStream(file); action.doSomethingWithFile(in); } catch (FileNotFoundException fnf) { // ハンドラへ処理を移す } finally { lock.unlock(); if (in != null) { try { in.close(); } catch (IOException e) { // ハンドラへ処理を移す } } } } } public final class Client { public void doSomething(File file) { ReentrantLockAction.doSomething(file, new LockAction() { public void doSomethingWithFile(InputStream in) { // オープンされたファイルに関する操作 } }); } }
違反コード (未チェック例外)
以下の違反コード例では、java.util.Date インスタンスを保護するために ReentrantLock を使用している。ちなみに java.util.Date はそもそもスレッドセーフではない。
final class DateHandler { private final Date date = new Date(); final Lock lock = new ReentrantLock(); public void doSomething(String str) { lock.lock(); String dateString = date.toString(); if (str.equals(dateString)) { // ... } // ... lock.unlock(); } }
doSomething() メソッドでは str が null 参照かどうかをチェックしていないため、実行時例外が発生する可能性がある。例外が発生した場合、獲得したロックが解放されないだろう。
適合コード (finally ブロック)
以下の適合コードでは、例外をスローする可能性のあるすべての操作を try ブロックの中で行い、対応する finally ブロック内でロックを解放している。そのため、実行時例外が発生したときにもロックは解放される。
final class DateHandler { private final Date date = new Date(); final Lock lock = new ReentrantLock(); public void doSomething(String str) { lock.lock(); try { String dateString = date.toString(); if (str != null && str.equals(dateString)) { // ... } // ... } finally { lock.unlock(); } } }
さらに、doSomething() メソッドでは、str が null 参照ではないことを確認し、NullPointerException がスローされないようにしている。
リスク評価
例外発生時にロックを解放しないと、スレッド飢餓状態(starvation)やデッドロックを引き起こす恐れがある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
LCK08-J | 低 | 高 | 低 | P9 | L2 |
関連する脆弱性
GERONIMO-2234 は、Geronimo アプリケーションサーバの脆弱性である。ユーザが keystore portlet をシングルクリックすると、何の警告もなくデフォルトの keystore がロックされる。これにより、サーバがクラッシュし、スタックトレースが出力される。さらに、ロックがクリアされないため、サーバはリスタートできない。
関連ガイドライン
MITRE CWE | CWE-883. Deadlock |
参考文献
[API 2006] | Class ReentrantLock |
翻訳元
これは以下のページを翻訳したものです。
LCK08-J. Ensure actively held locks are released on exceptional conditions (revision 84)