2023.01.23
JVM Synchronizatio
JVM Synchronization
(株)エクセムコンサルティング本部/APMチームキム・ジョンテ
1. 概要
この文書は、JAVAの特徴の1つであるMulti Thread環境での共有Resourceに対するThreadの競合とSynchronizationの内容について説明しています。
本文の内容を通じて、Java の同期装置である Monitor について理解し、さらに Java の Synchronization を適切に活用できる知識を提供する目的で作成してます。
1.1 1 JavaそしてThread
WAS(Web Application Server)では、多数の同時ユーザーを処理するために数十から数百
の Thread を使用します。
2つ以上のスレッドが同じリソースを使用する場合、必然的にスレッド間で競合が発生し、場合によってはデッドロックが発生する可能性があります。 Webアプリケーションでは、複数のThreadが共有リソースにアクセスすることは非常に頻繁にあります。 代表的にログを記録する時にも、ログを記録しようとするThreadがLockを獲得して共有リソースにアクセスします。 Dead Lock は Thread 競合の特別な場合であり、2 つ以上の Thread で作業を完了するために相手の作業が終わらなければならない状況を指します。
Thread競合のためにさまざまな問題が発生する可能性があり、このような問題を分析するためにはThread Dumpを利用することもあります。
各Threadの状態を正確に知ることができるからです。
1.2 Thread 同期
複数のスレッドが共有リソースを使用するときの整合性を確保するには、同期デバイスとして一度に1つスレッドだけが共有リソースにアクセスできるようにする必要があります。
Java では Monitor を利用して Thread を同期します。
すべてのJavaオブジェクトは1つのMonitorを持っています。
そしてMonitorは1つのスレッドしか所有できません。 特定の Thread が所有する Monitor を他の Thread が獲得するには
当 Monitor を所有している Thread が Monitor を解除するまで Wait Queue で待機しなければなりません。
1.3 Mutual Exclusionと Critical Section
共有データに複数の Thread が同時に接近して作業するとメモリ Corruption 発生することがあります。
共有データのアクセスは、一度に1つのスレッドで順番に行われるべきです。
誰かが共有データを使用するとき、他のスレッドは使用できないようにする必要があります。たとえば、書き込み可能な変数があります。
Heap には Object のメンバ変数 (Member Variable) があり、JVM は対応する Object
とClassをObject Lock(広義の概念)を使って保護します。
Object Lock は一度に 1 Thread だけ Object を使うように内部的に Mutex のようなものを活用します。 JVMがClass FileをLoadするとき、Heapにはjava.lang.classのinstanceが1つ生成され、Object Lockはjava.lang.class Objectのinstanceに同期することです。
この Synchronization は DBMS の Lock とちょっと違い Oracle DBMSの場合、SelectはExclusive Lockを使用しませんが(For update文を除く)、DMLの場合はExclusive Lockを渡します。
しかし、JAVAは、Threadが何をしていたとしても、Synchronizationが必要な地域に入ると、無条件にSynchronizationを実行します。 この地域をCritical Sectionといいますが、Threadがこの領域に入ると必ず同期操作を行います。
Thread が Object の Critical Section に入るときに同期を実行して Lock を要求する方式です。 Lock を獲得すると Critical Section で作業が可能で、Lock 獲得に失敗すると Lock
を所有する他の Thread が Lock を放すまで待機します。 ところでJAVAはObjectについて
ロックを重複して獲得することが可能です。
つまり、Threadが特定のObjectのCritical Sectionに入るたびにLockを取得する作業を再実行するということです。
Object の Header には Lock Counter を持っていますが、Lock を獲得すると 1 増加、離すと 1 減少します。
Lockを所有するThreadだけが可能です。 Thread は、一度実行に 1 つの Lock だけを獲得または放すことができます。
Countが0の場合、他のThreadがLockを獲得でき、Threadが繰り返しLockを獲得するとCountが増加します。
Critical Section は Object Reference と連携して同期を行います。 Thread は、 Critical Section の最初の Instruction を実行するときに参照する Object について Lockを獲得しなければなりません。 Critical Sectionを離れると、Lockは自動的にリリースされ、明示的な操作は
必要ありません。 JVMを利用する人々は、単にCritical Sectionを指定してくれば同期は自動的に行われるということです。
1.4 Monitor
Javaは基本的にMulti Thread環境を前提として設計されており、同期の問題を解決するためのものです。
本質的なメカニズムを提供します。 Java 内のすべての Object は必ず 1 つの Monitor を持ちます。 上記のオブジェクトロックはモニタに対応します。 特定の Object の Monitor には同時に 1 つの Thread だけが入ることができます(Enter) 。 他の Thread によってすでに占有されている Monitor に入ろうとする Thread は Monitor の Wait Set で待機します。 JavaでMonitorを占有する唯一の方法は、Synchronizedキーワードを使用することです。 Synchronized StatementとSynchronized Methodの2つの方法があります。 Synchronized Statement は Method 内の特定の Code Block に Synchronized キーワードを使用したものです。
2. Javaの同期方法
JAVA は Monitor という Synchronization メカニズムを使用しますが、 Monitor は特定の Object や特定 Code Block にかかる一種の Lock と考えても構いません。 JAVAはモニターを排他的目的(Mutual Exclusion)以外の共同作業(Cooperation)のために使用することもあります。
2.1 Synchronized Statement
… 생략 …
private int[] intArr = new int[10]; void synchBlock() {
synchronized (this) {
for (int i =0 ; i< intArr.length ; ++i ) { intArr(i) = i;
}
}
}
… 省略…
Thread は for 構文の実行中に Object の Monitor を占有します。 そのObjectに対してMonitorを占有しようとするすべてのThreadは、for構文の実行中に待機状態(BLOCKED)に陥ります。 先に説明したように、Byte Code を見ると MONITORENTER, MONITOREXIT という Code を見ることができます(略)。
Synchronized Statement では、これを行う Current Object を対象に Monitor Lock を実行します。 Byte Code で MONITORENTER が実行されると、Stack の Object Reference を使って参照された(this) Object に対する Lock を取得する作業を行なう。 ロックをすでに獲得している場合は、ロックカウントを1つ増やし、もし初めてロックを獲得するなら、ロックカウントを1にしてロックを所有することになります。 Lockを獲得する状況でなければ、Lockを獲得するまでBLOCKED状態で待機することになります。 MONITOREXITが実行されるとLock Countが減少し、値が0に達するとLockを解放します。 MONITOREXITはExceptionを投げる直前にCritical Sectionを抜けるために使用されますが、Synchronized Statementの使用は内部的にtry〜catch句を使用する効果があるといいます。 Monitorに入った後、希望のコードを実行して再びMonitorを抜け出すことがJavaが同期を実行する方法といえます。
2.2 Synchronized Method
… 省略 …
class SyncMtd {
private int[] intArr = new int[10]; synchronized void syncMethod() {
for (int i = 0 ; i < intArr.length ; ++i) { intArr[i] = i;
}
}
}
… 생략 …
Synchronized Method は、Method を宣言する際に Synchronized アクセシビリティ (Qualifier) を使用する方式です。 Synchronized Statement 方式と異なり、Byte Code に Monitor Lock 関連内容がありません(MONITORENTER, MONITOREXIT)。 Synchronized Method のためモニターロックの使用可否は、メソッドの symbolic reference を resolution する過程で決定されるからなのです。 これは、Methodの内容がCritical Sectionではなく、Methodの呼び出し自体がCritical Sectionであることを意味します。 Synchronized Statement はランタイム時点にMonitor Lockを取得しますが、Synchronized MethodはこのMethodを呼び出すためにLockを取得する必要があります。
Synchronized MethodがInstance Methodの場合、Methodを呼び出すthis Objectに対してLockを取得する必要があります。 Class Method(Static Method) なら、この Method が属するクラス、つまりその Class の Class Instance(Object) に対して Lock を獲得しなければなりません。
Synchronized Methodが正常に実行されているかどうかにかかわらず終了するだけで、JVMはLockを自動的にリリースします。
2.3 Wait And Notify
あるスレッドは特定のデータを必要とし、もう一方のスレッドは特定のデータを提供する場合
Monitor Lockを使用してスレッド間のCooperation操作を実行できます。
メッセンジャープログラムの場合、クライアントからネットワークを通じて相手のメッセージを受け取るListener Threadと受信したメッセージを示すReader threadがあると仮定してみましょう。 Reader Thread は Buffer のメッセージをユーザに表示し、Buffer を再び空にしてバッファにメッセージが入るまで待機するようになります。 Listener Thread は Buffer にメッセージを記録し、ある程度記録が終わると Reader Thread にメッセージが入ったことを知らせ、Reader Thread がメッセージを読み取る作業を実行できるようにします。 この時 Thread間にはWait and Notify型の Monitor Lock を使用します。 WaitとNotify Methodを使用して同期を実行する方法は、Synchronized方式の「アプリケーション」と呼ばれることがあります。
上の図はCooperationのためのMonitor Lockを表現したものです。
Reader ThreadはMonitor Lockを所有していてバッファを空にしてからwait()を実行します。 自分が所有するMonitorをしばらく置いて、このMonitorを待機するWait Setに入ります。 リスナースレッドはメッセージを受信し、それをユーザーが読む必要があるときにnotify()を実行してウェイトセットから出ることができるというシグナルを知らせると(ロックリリースではない)、リーダースレッドはモニターロックのすぐに取得できないかもしれません。
Listener Threadが自発的にMonitor Lockを放さないと、誰もLockを獲得できないです。 Listener Threadがnotify()の後にLockを放すと、Reader Threadは再びMonitor Lockをキャッチしようとします。 スレッド間の共同作業もMutual exclusionのようにObject Lockを使用します。 つまり、 Thread は特定の Object Class の wait(), notify() などのメソッドを通じて Monitor Lock を使用します。
Thread が Entry set に入るとすぐに Monitor Lock 獲得を試みた他の Thread が Monitor Lock を獲得した場合、後発 Thread は再び Entry set で待機しなければなりません。
Monitor Lock を獲得して Critical Section コードを実行する Thread は Lock を置いていく方法もしくは Wait set に入る方法があります。
Monitor Lock を所有する Thread が作業実行中に waitを実行すると、獲得した Monitor Lock を置いて Wait set に入る。
ところで、この Thread が wait() だけを実行して Wait set に入ると、この Monitor Lock を獲得する権限は Entry set の Thread だけに与えられる。
すると、Entry set の Thread は互いに競い合い、Monitor Lock の所有者になる。 したがって、 notify(), notifyAll() を実行しなければ、Entry set と Wait set にある Thread が競合することになります。 notify() は Wait set の Thread のいずれか 1 つの Thread だけを Monitor Lock 競合に参加させるもので、 notifyAll() は Wait set 内のすべてのスレッドを競争に参加させるものです。
Wait set に入った Thread が Critical Section を抜ける方法は Monitor を再獲得して Lock を離していく方法以外にはありません。 モニターロックはJVMを実装したベンダーによって異なりますが、Java同期の基本であるモニターロックはパフォーマンス上の理由で頻繁に使用しないことが傾向です。
現在、私たちが使用しているほとんどのJVMは、前述のMonitor LockをHeavy-weight Lock、Light-weight Lockに分割します。 これは、実装をする方式を言い、light-weight Lock は Atomic operation を利用した軽い Lock として Mutex のような OS のリソースを使わず、内部の Operation だけで同期を処理して Monitor Lock に比べて軽いという利点があります。
light-weight Lock はほとんどの Object の場合 Thread 間の競合が発生しないという点に着目し、もし Thread 間競合なしに自分が Lock を所有したまま Object の Critical Section に入ると light-weight Lock を使用して Monitor enter 、モニタ出口を実行します。 ただし、Thread間競合が発生すると、以前のheavy-weight Lockに回帰する仕組みです。 light-weight Lockもベンダーごとに少しずつ異なって実装されています。
3. Synchronized StatementとSynchronized Method 使用
複数の Thread が同時に Access できるオブジェクトは無条件 Synchronized Statement/Method で保護すべきでしょうか? いつもそうではありません。 Synchronizedを実行するコードとそうでないコードのパフォーマンスの違いは非常に大きく、同期のためにMonitorにアクセスすることにはオーバーヘッドがあります。 必ず必要な場合にのみ使用してください。 以下の例を見てみましょう。
private static Instance= null;
public static Synchronized getInstance() { if(Instance== null)
{ Instance= new Instance(…); } return instance;
}
Singleton方式を実装するために getInstance Method を Synchronized でうまく保護しましたが、不要な性能低下があります。 Instance変数が実行中に変更される可能性がない場合、上記のコードは非効率的です。
4. Thread 状態
スレッドダンプを分析するには、スレッドの状態を知る必要があります。 Thread の状態は java.lang.Thread クラス内部に State という名前を持つ Enumerated Types (列挙型) として宣言されています。
l NEW:スレッドが作成されたがまだ実行されていない状態
RUNNABLE: 現在 CPU を占有し、操作を実行している状態。 オペレーティングシステムの資源配布のため
WAITING状態になることもあります。
BLOCKED:モニターを取得するために他のスレッドがロックを解除するのを待っている状態
WAITING: wait() Method, join() Method, park() Method などを利用して待機している状態
TIMED_WAITING: sleep() Method, wait() Method, join() Method, park() Method などを利用して待機している状態。 WAITING状態との違いは、Methodの引数で最大待機時間を明示することができ、外部的な変化だけでなく時間によってもWAITING状態が解除されることができるということです。
これらの状態の正確な理解は、スレッド間のロック競合を理解するために不可欠です。 特定のスレッドが特定のオブジェクトのモニタを長時間占有している場合、同じモニタを必要とする他のすべてのスレッドはBLOCKED状態で待機します。 この現象が過度になると、Thread暴走が発生し、システム障害を引き起こす可能性があります。 このような現象はWait Methodを利用して待機をする場合も同様です。
特定のスレッドが長時間通知を介して待機状態のスレッドを起動しないと、多数のスレッドが待機状態またはTIMED_WAITING状態で待機します。
5. Threadの種類
Javaスレッドは、デーモンスレッド(デーモンスレッド)と非デーモンスレッド(非デーモンスレッド)に分けることができます。 デーモンスレッドは、他の非デーモンスレッドがない場合は動作を停止します。 ユーザーが直接スレッドを生成しなくても、Javaアプリケーションは基本的に複数のスレッドを生成します。 ほとんどがデーモンスレッドですが、Garbage Collectionや、JMXなどの作業を処理するためのものです。
‘static void main(String[] args)’ Method が実行される Thread は非デーモン Thread で生成され、この Thread が動作を停止すると他のデーモン Thread も同様に動作を停止することになります。 もう少し詳しく分類すれば以下の通りになります。
VM Background Thread:Compile、Optimization、Garbage Collectionなど、JVM内部の仕事を行うBackground Thread。
Main Thread: main(String[] args) Method を実行する Thread でユーザーが明示的に Thread を実行しなくても JVM は 1 つの Main Thread を生成して Application を駆動します。 Hot Spot JVM では VM Thread という名前が付与されます。
User Thread: ユーザーによって明示的に生成された Thread です。 java.lang.Thread を継承 (extends) 受け取るか、 java.lang.Runnable インタフェースを実装 (implements) することで User Thread を生成することができます。
6. JVMにおける大気現象の分析
Java / WAS環境では、OWIなどの体系的な方法論はありません。 Java ではさまざまな方法を提供しています
6.1 Javaで提供する方法
- Thread Dump, GC Dump のような基本的なツール
- BCI(Byte Code Instrumentation) + JVMPI/JVMTI ( C Interface )
Java 5 で標準で採用された JMX の Platform MXBean、JVMpi/ti を通じて得られた情報は簡単に得ることができますが、まだ不足している面が多くあります。
6.2 WASで提供する方法
ほとんどのWASがユーザー要求を効果的に処理するために、Thread Pool、Connection
プール、EJB Pool / Cacheなどの概念を実装しました。 ほとんどのWASがこの種のパフォーマンス情報(Pool/Cacheなどの使用量)をJMX APIを通じて提供(Expose)し、気をつければ自分だけのパフォーマンスリポジトリを作成することもできます。
6.3 非効率なソースチューニングによる Side Effect
ApplicationがLoopを回しながらDMLを実行する構造で、そのタスクを複数のThreadが同時に実行すると、Oracleチューナーがこれを把握し、すべてのアプリケーションをバッチ実行に変換するように促しました(つまり、PreparedStatement.addBatch、executeBatchを使用します)。
DBとの通信が大幅に減少し、DB操作自体の量も減少します。 つまり、アプリケーションの立場から見ると、Wait Time(DB I/O Time)が減るため、当然ユーザーの Response Time は減らさなければなりません。 しかし、結果はApplicationで極端な性能低下が発生してしまいました。 その理由は2つあります。 まず、Batch ExecutionはApplicationでより多くのメモリを要求します。
これにより、Garbage Collectionが大量に発生します。 2つ目は、Batch Executionは1回のOperationにConnectionを保持する時間がもう少し長いです。 したがって、より多くのConnectionが必要であり、その分Connection Poolがすぐに使い果たされます。 つまり、Wait Timeを減らそうとする試みが別のSide Effectを呼び出し、これによって他の種類のWait Time(GC Pause TimeとConnection Pool待ち時間)が増加した場合です。 同期メカニズムは、同時セッション/ユーザー/スレッドをサポートするシステムで一般的に使用されます。 WAS Applicationでは、数十から数百のスレッドが同じリソースを獲得するために競合していますが、この過程で同期の問題が発生し、待機現象(Wait)も発生する可能性があります。
7. Thread Dump
Javaでスレッド同期の問題を分析するための最も基本的なツールとして、現在使用されているスレッドの状態とスタックトレースを出力し、さらにJVMの種類に応じてより豊富な情報を提供します。
7.1Thread Dump 生成方法
Unix シリーズ: kill -3 [PID]
Windowsシリーズ:現在のコンソールでCtrl + Break。
共通: jstack [PID]
7.2 Thread ダンプの情報
取得したスレッドダンプには次の情報が含まれています。
“pool-1-Thread-13” prio=6 tid=0x000000000729a000 nid=0x2fb4 runnable [0x0000000007f0f000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129) at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:264) at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:306) at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158)
- Locked <0x0000000780b7e688> (a java.io.InputStreamReader) at java.io.InputStreamReader.read(InputStreamReader.java:167) at java.io.BufferedReader.fill(BufferedReader.java:136)
at java.io.BufferedReader.readLine(BufferedReader.java:299)
- Locked <0x0000000780b7e688> (a java.io.InputStreamReader) at java.io.BufferedReader.readLine(BufferedReader.java:362)
Thread名:Threadの識別名。 java.lang.Thread クラスを使って Thread を生成すると、 Thread-(Number) 形式で Thread 名が生成されます。 java.util.concurrent.ThreadFactoryクラスを使用すると、pool-(number)-Thread-(number)形式でスレッド名が生成されます。
●優先順位:スレッドの優先順位
- Thread ID: Thread の ID。 この情報を利用して、ThreadのCPU使用、メモリ使用など有用な情報を得ることができます。
- スレッド状態:スレッドの状態。
- Threadコールスタック:スレッドのコールスタック情報。
8. Case 別 Synchronized の Thread Dump 分析
Case1: Synchronized による同期
public class dump_test {
static Object Lock = new Object(); public static void main(String[] args) {
new Thread2().start(); try {
Thread.sleep(10);
} catch (Exception ex) {
}
new Thread1().start(); new Thread1().start(); new Thread1().start();
}
}
class Thread1 extends Thread { int idx = 1;
public void run() { while (true) {
Synchronized (dump_test.Lock) { // Thread1 은 Synchronized ブロックによる
Thread2 의 作業が終了するのを待つ.
System.out.println(idx++ + ” loopn”);
}
}
}
}
class Thread2 extends Thread { public void run() {
while(true) {
Synchronized(dump_test.Lock) { // Thread2 는 Synchronized ブロックを使って長い(Long) タスクを実行する.
for(int idx=0; idx<=”” idx++)=””>
}
}
}
}
Case2: wait/notify による同期
public class dump_test2 {
static Object Lock = new Object(); public static void main(String[] args) {
new Thread2().start(); try {
Thread.sleep(10);
} catch (Exception ex) {} new Thread1().start(); new Thread1().start(); new Thread1().start();
}
}
class Thread1 extends Thread { int idx = 1;
public void run() { while (true) {
Synchronized (dump_test2.Lock) { System.out.println(idx++ + ” loopn”); try {
dump_test2.Lock.wait();
} catch (Exception ex) {} // Wait Method を使用してnotifyが行われる
待つ.
}
}
}
}
class Thread2 extends Thread { public void run() {
while (true) {
for (int idx = 0; idx < 90000000; idx++) {
}
Synchronized (dump_test2.Lock) {dump_test2.Lock.notify(); // notify Methodを利用して WAITING 状態の Thread を起こす。
}
}
}
}
Case1(Synchronized) では Thread1 が BLOCKED 状態になり、 Case2(Wait/Notify) では Thread1 が WATING 状態になります。 Javaで明示的にスレッドを同期させる方法は、この2つのケースのみです。 スレッドプール同期によるスレッド待機、JDBC Connectionプール同期によるスレッド待機、EJBキャッシュ/プール同期によるスレッド待機
など、すべてのスレッド待機がこの2つのケースですべて解釈可能です。 上記の2つのケースについて、ベンダーごとにThread Dumpでどのように観察されるかを見てみましょう。
- Hot Spot JVM
8.1.1 Case1: Synchronized による同期
Full Thread dump Java HotSpot(TM) 64-Bit Server VM (1.5.0_04-b05 mixed mode):
“DestroyJavaVM” prio=1 tid=0x0000000040115580 nid=0x1e18 waiting on condition [0x0000000000000000..0x0000007fbfffd380]
“Thread-3” prio=1 tid=0x0000002afedbd330 nid=0x1e27 waiting for Monitor Entry [0x00000000410c9000..0x00000000410c9bb0]
at Thread1.run(dump_test.java:22)
- waiting to Lock <0x0000002af44195c8> (a java.lang.Object)
“Thread-2” prio=1 tid=0x0000002afeda6900 nid=0x1e26 waiting for Monitor Entry [0x0000000040fc8000..0x0000000040fc8c30]
at Thread1.run(dump_test.java:22)
- waiting to Lock <0x0000002af44195c8> (a java.lang.Object)
“Thread-1” prio=1 tid=0x0000002afeda5fe0 nid=0x1e25 waiting for Monitor Entry [0x0000000040ec7000..0x0000000040ec7cb0]
at Thread1.run(dump_test.java:22)
- waiting to Lock <0x0000002af44195c8> (a java.lang.Object)
“Thread-0” prio=1 tid=0x0000002afeda3520
nid=0x1e24 runnable [0x0000000040dc6000..0x0000000040dc6d30] at Thread2.run(dump_test.java:38)
- waiting to Lock <0x0000002af44195c8> (a java.lang.Object)
Synchronized による Thread ブロッキングが発生する途中の Thread dump 結果です。 Thread-1、Thread-2、Thread-3が「waiting for Monitor Entry」状態です。 つまり Synchronized 文によってブロックされて Monitor に入るのを待っている状態です。
この場合 Thread.getState() Method は BLOCKED 値を Return する一方 Thread-0 は現在 “runnable” 状態で仕事をしているところです。
また、Thread-0とThread1,2,3が同じ0x0000002af44195c8に対して競合をしています。
8.1.2 Case2: Wait/Nofity による同期
Full Thread dump Java HotSpot(TM) 64-Bit Server VM (1.5.0_04-b05 mixed mode):
“DestroyJavaVM” prio=1 tid=0x0000000040115580 nid=0x1c6c waiting on condition [0x0000000000000000..0x0000007fbfffd380]
“Thread-3” prio=1 tid=0x0000002afedb7020 nid=0x1c7b in Object.wait() [0x00000000410c9000..0x00000000410c9db0]
at java.lang.Object.wait(Native Method)
- waiting on <0x0000002af4442a98> (a java.lang.Object) at java.lang.Object.wait(Object.java:474)
at Thread1.run(dump_test2.java:23)
- Locked <0x0000002af4442a98> (a java.lang.Object)
“Thread-2” prio=1 tid=0x0000002afedb5830 nid=0x1c7a in Object.wait() [0x0000000040fc8000..0x0000000040fc8e30] at java.lang.Object.wait(Native Method)
- waiting on <0x0000002af4442a98> (a java.lang.Object) at java.lang.Object.wait(Object.java:474)
at Thread1.run(dump_test2.java:23)
- Locked <0x0000002af4442a98> (a java.lang.Object)
“Thread-1” prio=1 tid=0x0000002afeda6d10 nid=0x1c79 in Object.wait() [0x0000000040ec7000..0x0000000040ec7eb0] at java.lang.Object.wait(Native Method)
- waiting on <0x0000002af4442a98> (a java.lang.Object) at java.lang.Object.wait(Object.java:474)
at Thread1.run(dump_test2.java:23)
- Locked <0x0000002af4442a98> (a java.lang.Object)
“Thread-0” prio=1 tid=0x0000002afeda3550
nid=0x1c78 runnable [0x0000000040dc6000..0x0000000040dc6b30] at Thread2.run(dump_test2.java:36)
Hot Spot VM で Wait/Notify による Thread ブロッキングが発生する途中の Thread dump 結果です。 Synchronized による Thread ブロッキングのケースと異なり、BLOCKED 状態ではなく WAITING 状態で待機します。
ここで特に注意すべきことは、Thread1、2、3を実際にブロックしているThreadが正確にどのThreadなのか直感的にわからないということです。
Thread1、2、3は待機状態にありますが、これはブロッキングによるものではなく、Notifyが来るのを待つ(Wait)だけだからです。
BLOCKED状態とWAITING状態の正確な違いを理解する必要があります。
ちなみに、BLOCKED状態とWAITING状態の正確な違いを理解するには、次のコードが意味するものを理解する必要があります。
Synchronized(LockObject) { LockObject.wait(); doSomething();
}
上記のコードが意味するものは次のとおりです。 Lock Object の Monitor にまず入ります。
Lock Objectに対する占有権を放棄し、MonitorのWait Set(待機リスト)で待機します。
他の Thread が Notify をしてくれたら Wait Set から出て再び Lock Object を占有します。 他のスレッドがすでにロックオブジェクトを占有している場合は、再びウェイトセットで待機します。 Lock Object を占有したまま doSomething() を実行し、Lock Object の Monitor から抜け出します。 つまり、Lock Object.wait() Method 呼び出しを通じて待機している状態では、すでに Lock Object に対する占有権を放棄した (Release) 状態であるため、BLOCKED 状態ではなく WAITING 状態に分類されるのに対し Synchronized 文によって Monitor にまだ 入らない状態ではBLOCKED状態に分類されます。
8.2IBM JVM
IBM JVMのThread Dumpは、Hot Spot JVMに比べて非常に豊富な情報を提供するのが簡単です。
Threadの現在の状態だけでなく、JVMの状態に関するさまざまな情報を提供します。
8.2.1 Case1: Synchronized による同期
// モニター情報
1LKMONPOOLDUMP Monitor Pool Dump (flat & inflated Object-Monitors):
…
2LKMONINUSE sys_mon_t:0x3003C158 infl_mon_t: 0x00000000:
3LKMONObject java.lang.Object@30127640/30127648: Flat Locked by Thread ident 0x08, Entry Count 1 //<– オブジェクト 0x08 Thread によってLocking
3LKNOTIFYQ Waiting to be notified: // <– 三つ Thread 待っています
3LKWAITNOTIFY “Thread-1” (0x356716A0)
3LKWAITNOTIFY “Thread-2” (0x356F8020)
3LKWAITNOTIFY “Thread-3” (0x3577FA20)
…
// Java Object Monitor 情報
1LKOBJMONDUMP Java Object Monitor Dump (flat & inflated Object-Monitors):
…
2LKFLATLockED java.lang.Object@30127640/30127648
3LKFLATDETAILS Locknflags 00080000 Flat Locked by Thread ident 0x08, Entry Count 1
…
// Thread リスト
1LKFLATMONDUMP Thread identifiers (as used in flat Monitors): 2LKFLATMON ident 0x02 “Thread-4” (0x3000D2A0) ee 0x3000D080
2LKFLATMON ident 0x0B “Thread-3” (0x3577FA20) ee 0x3577F800
2LKFLATMON ident 0x0A “Thread-2” (0x356F8020) ee 0x356F7E00
2LKFLATMON ident 0x09 “Thread-1” (0x356716A0) ee 0x35671480
2LKFLATMON ident 0x08 “Thread-0” (0x355E71A0) ee 0x355E6F80 <– 30127640/30127648を
占めている 0x08 Thread の名前Thread-0
…
// Threrad Stack Dump
2XMFULLTHDDUMP Full Thread dump Classic VM (J2RE 1.4.2 IBM AIX build ca142-20050929a (SR3), native Threads):
3XMThreadINFO “Thread-4” (TID:0x300CB530, sys_Thread_t:0x3000D2A0, state:CW, native ID:0x1) prio=5 // <– Conditional Wait状態
3XHNATIVESTACK Native Stack NULL ————
3XHSTACKLINE at 0xDB84E184 in xeRunJavaVarArgMethod
…
3XMThreadINFO “Thread-3” (TID:0x300CB588, sys_Thread_t:0x3577FA20, state:CW, native ID:0xA0B) prio=5
4XESTACKTRACE at Thread1.run(dump_test.java:21)
…
3XMThreadINFO “Thread-2” (TID:0x300CB5E8, sys_Thread_t:0x356F8020, state:CW, native ID:0x90A) prio=5
4XESTACKTRACE at Thread1.run(dump_test.java:21)
…
3XMThreadINFO “Thread-1” (TID:0x300CB648, sys_Thread_t:0x356716A0, state:CW, native ID:0x809) prio=5
4XESTACKTRACE at Thread1.run(dump_test.java:21)
…
3XMThreadINFO “Thread-0” (TID:0x300CB6A8, sys_Thread_t:0x355E71A0, state:R, native ID:0x708) prio=5 // <– Lock ホルダー
4XESTACKTRACE at Thread2.run(dump_test.java(Compiled Code)) 3XHNATIVESTACK Native Stack
NULL ———— 3XHSTACKLINE at 0x344DE720 in
…
Thread-0(ident=0x08) Thread が java.lang.Object@30127640/30127648 オブジェクトに対して Monitor Lock を占有して実行 (state:R) 中であり、残りの 3 つの Thread “Thread 1,2,3” は 同じオブジェクトに対してロックを取得するための待機状態です。.
8.2.2 Case2: Wait/Nofity による同期
Wait/Nofity による同期によって Thread ブロッキングが発生する場合には 1 つの事実を除いては Case1 とまったく同じです。
Wait/Notify による同期の場合、実際に Lock を占有している Thread は存在せず、Nofity してくれるのを待つだけです。
したがって、Lockを占有しているThreadがどのThreadであるかに関する情報はThread Dumpには現れません。
以下は、Wait/Notify による Thread 同期が発生する状況の Thread Dump の一部です。 Wait/Notify による Thread 同期の場合には Lock を占有している Thread の正体がすぐに分からないのです。 (ブロッキングではなく単純な待機だから)
…
1LKMONPOOLDUMP Monitor Pool Dump (flat & inflated Object-Monitors):
…
2LKMONINUSE sys_mon_t:0x3003C158 infl_mon_t: 0x3003BAC0:
3LKMONObject java.lang.Object@30138C58/30138C60: // <– Object に対して Waiting Thread が存在するが Locking されていない!!!
3LKNOTIFYQ Waiting to be notified: 3LKWAITNOTIFY “Thread-3” (0x3577F5A0)
3LKWAITNOTIFY “Thread-1” (0x355E7C20)
3LKWAITNOTIFY “Thread-2” (0x356F7A20)
9. Thread Dumpによるスレッド同期のトラブルシューティングの実例
実際の運用環境でパフォーマンス問題が発生した場合に抽出したもので、Thread Dumpを分析した結果、多数のWorker Threadが次のようにブロックされています。
“http8080-Processor2” daemon prio=5 tid=0x042977b0 nid=0x9a6c in Object.wait() [503f000..503fdb8]
at java.lang.Object.wait(Native Method)
- waiting on <0x17c3ca68> (a org.apache.commons.pool.impl.GenericObjectPool) at java.lang.Object.wait(Object.java:429)
at org.apache.commons.pool.impl.GenericObjectPool.borrowObject(Unknown Source)
- Locked <0x17c3ca68> (a org.apache.commons.pool.impl.GenericObjectPool) at org.apache.commons.dbcp.PoolingDriver.connect(PoolingDriver.java:146) at java.sql.DriverManager.getConnection(DriverManager.java:512)
- Locked <0x507dbb58> (a java.lang.Class)
at java.sql.DriverManager.getConnection(DriverManager.java:193)
- Locked <0x507dbb58> (a java.lang.Class)
at org.jsn.jdf.db.commons.pool.DBManager.getConnection(DBManager.java:40) at org.apache.jsp.managerInfo_jsp._jspService(managerInfo_jsp.java:71)
…
at org.apache.tomcat.util.Threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683) at java.lang.Thread.run(Thread.java:534)
“http8080-Processor1” daemon prio=5 tid=0x043a4120 nid=0x76f8 waiting for Monitor Entry [4fff000..4fffdb8]
at java.sql.DriverManager.getConnection(DriverManager.java:187)
– waiting to Lock <0x507dbb58> (a java.lang.Class)
at org.jsn.jdf.db.commons.pool.DBManager.getConnection(DBManager.java:40) at org.apache.jsp.loginOK_jsp._jspService(loginOK_jsp.java:130)
at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:137) at javax.servlet.http.HttpServlet.service(HttpServlet.java:853)
at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:210) at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:295)
at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:241)
…
at org.apache.tomcat.util.Threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683) at java.lang.Thread.run(Thread.java:534)
…
上記の Thread Dump を分析してみると java.sql.DriverManager.getConnection() 内部から Connection を取得する過程で Synchronized による Thread ブロッキングが発生しました。 org.apache.commons.pool.impl.GenericObjectPool.borrowObject() 内部で Connection を取得する過程で Wait/Notify による Thread ブロッキングが発生します。
つまり、Connection PoolでConnectionを取得する過程でスレッド競合が発生したことは、現在Connection Poolの完全に使い果たされ、これにより新しいDB Requestに対して新しいConnectionを結ぶ過程で性能低下現象が生じたということです。
Connection Poolの最大Connection数が低く設定されていると、待機現象はさらに激しくなります。
他の Thread が DB Request を終えて Connection を離すまで待たなければならないからです。 解決策は? Connection Poolの初期Connection数と最大Connection数を増やします。 実際に発生するDB Requestの数は少なく、Connection Poolがすぐに使い果たされると、Connectionを閉じない問題である可能性が高いです。
この場合、ソース検証または監視ツールを介してConnectionを開閉するロジックが正常に動作していることを確認する必要があります。
ちなみに、幸いなことに、iBatisやHibernateなどのフレームワークが普遍的に使用されている間、JDBC Connectionを誤って扱う問題はほとんどなくなっています。