違和感を覚えたのは、次の個所だ。
ログ1 スタックトレース1(get)の強調版 |
at troublehack11.DBConnectionPool.get()(DBConnectionPool.java:27) |
ログ3 スタックトレース2(wait)の強調版 |
at java.lang.Object.wait(Native Method) |
なんと、ほかのスレッドが待たされている排他制御個所と、このスレッドが待ち状態になっている個所で、利用されているロック用のオブジェクトが異なるのだ。
Javaで排他制御を必要とするプログラムを書く際に見落としがちなものの1つに、waitメソッドを利用した際のスレッドの待機時での、ロックオブジェクトに対するロック状態がある。ここでいったん、次のサンプルプログラムを見てみよう。
プログラム1 2重ロック |
public class DualLock implements Runnable { |
賢明な読者なら、このプログラムの問題点がすぐに分かったと思う。このサンプルプログラムは、2つのスレッドがlock1、lock2の順番でロックを獲得しながら、最後にlock2でwait状態へと遷移するプログラムだ。
ロックを獲得する順番が、2つのスレッド間で同じであるため、このプログラムはデッドロックを引き起こさない。そのため直感的には、2つのスレッドはともにwait状態へと遷移してほしいところだ。
さて、このプログラムのどこに問題が発生するかを説明しよう。
ロックにかかわるJavaのObject.wait()メソッドの仕様を確認すると、次のような記述がある。
「現在のスレッドはこのオブジェクトのモニターのオーナでなければなりません。スレッドはこのモニターの所有権を解放し、別のスレッドが notify メソッドまたは notifyAll メソッドを呼び出してこのオブジェクトのモニター上で待機するスレッドに通知を出すまで待機します。そのあと、スレッドはモニターの所有権を再度取得するまで待機してから実行を再開します」(引用:ObjectクラスのwaitメソッドのAPIドキュメント)
ここでは、Object.wait()呼び出し時には、Objectのロックを解放すると記載されている。逆にいえば、waitメソッドを呼び出したObject以外の取得済みロックについては解放しないことを意味している。そのため、上記プログラム1を実行すると、次のようなスレッドダンプを得ることになる。
ログ4 2重ロックのスレッドダンプ |
"Thread2" prio=6 tid=0x018e2400 nid=0x1454 waiting for monitor entry [0x03c0f000..0x03c0fd00] |
このスレッドダンプを読むと、Thread1が「lock2(0x238b5cd8)」でwait状態へと遷移するものの、「lock1(0x238b5cd0)」のロックを解放しないため、Thread2は、lock2でロック獲得待ちのためブロックされている。
気を付けていれば、このようなミスが発生することは少ないと思われるかもしれない。
では、次のプログラムではどうだろうか。
プログラム2 隠れ2重ロック |
private Object lock; |
一見、何も問題がないように見えるだろう。しかし、多くのAPIドキュメントには、スレッドがロックを保持してwait状態へと遷移するか否かという記述はない。そのため、SomeClassクラスのsomeMethodメソッドのソースコードを確認せずに、同じような事象が発生しないと自信を持って答えることは難しい。
このように、2重ロックは非常に気持ちの悪い違和感を覚える実装だ。しかし、必ずしも問題が発生する訳ではない。また、システムがレスポンスを返さなくなるという問題の原因特定には至っていない。
そこで、さらに詳細にスレッドダンプを解析することにした。
Javaアプリケーション内の動作状況の全貌(ぜんぼう)を把握するため、筆者はよくスタックトレースによるスレッドの分類と集計を行っている。すべてのスレッドを、その状態ごとに分類し、集計していく作業だ。今回は、Samuraiで赤く表示されているスタックトレースを1つ1つ確認することで、次のような分類と集計を得た。
スタックトレース種別 | 1回目のスレッドダンプ | 2回目 | 3回目 | |
---|---|---|---|---|
DBCPにて待機 | 1 | 1 | 1 | |
DBConnectionPool.get | 34 | 42 | 55 | |
DBConnectionPool.release | 15 | 15 | 15 | |
これまで確認したスタックトレースとは異なるスレッドが存在した。「DBConnectionPool.release」である。この状態のスレッドは、事象発生後のスレッドダンプよりずっと同じ数だけ存在しており、次のスタックトレースを出力していた。
ログ5 スタックトレース3(release) |
"TP-Processor-80" prio=6 tid=0x01b16400 nid=0xbf0 waiting for monitor entry [0x055bf000..0x055bfb80] |
このスタックトレースを確認した瞬間、さらなる違和感を体験することとなる。
Copyright © ITmedia, Inc. All Rights Reserved.