Javaのデッドロックの例

Javaのデッドロックは、2つ以上のスレッドが永久にブロックされるプログラミング状況です。Javaのデッドロック状況は、少なくとも2つのスレッドと2つ以上のリソースが存在する場合に発生します。ここでは、Javaのデッドロックシナリオを引き起こす単純なプログラムを作成し、その分析方法を見てみましょう。

Javaのデッドロック

次に、Javaのスレッドでデッドロックを作成するプログラムを見てみましょう。

package com.journaldev.threads;

public class ThreadDeadlock {

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();
    
        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");
        
        t1.start();
        Thread.sleep(5000);
        t2.start();
        Thread.sleep(5000);
        t3.start();
        
    }

}

class SyncThread implements Runnable{
    private Object obj1;
    private Object obj2;

    public SyncThread(Object o1, Object o2){
        this.obj1=o1;
        this.obj2=o2;
    }
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name + " acquiring lock on "+obj1);
        synchronized (obj1) {
         System.out.println(name + " acquired lock on "+obj1);
         work();
         System.out.println(name + " acquiring lock on "+obj2);
         synchronized (obj2) {
            System.out.println(name + " acquired lock on "+obj2);
            work();
        }
         System.out.println(name + " released lock on "+obj2);
        }
        System.out.println(name + " released lock on "+obj1);
        System.out.println(name + " finished execution.");
    }
    private void work() {
        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上記のプログラムでは、SyncThreadがRunnableインターフェースを実装し、synchronizedブロックを使用して一度に1つずつロックを取得する2つのオブジェクトで動作します。mainメソッドでは、SyncThread用に3つのスレッドが実行され、各スレッド間には共有リソースがあります。スレッドは、最初のオブジェクトに対してロックを取得できるように実行されますが、2番目のオブジェクトに対してロックを取得しようとすると、すでに他のスレッドによってロックされているため、待機状態になります。これにより、スレッド間のリソース間で循環依存が形成され、デッドロックが発生します。上記のプログラムを実行すると、以下の出力が生成されますが、デッドロックのためプログラムは終了しません。

t1 acquiring lock on java.lang.Object@6d9dd520
t1 acquired lock on java.lang.Object@6d9dd520
t2 acquiring lock on java.lang.Object@22aed3a5
t2 acquired lock on java.lang.Object@22aed3a5
t3 acquiring lock on java.lang.Object@218c2661
t3 acquired lock on java.lang.Object@218c2661
t1 acquiring lock on java.lang.Object@22aed3a5
t2 acquiring lock on java.lang.Object@218c2661
t3 acquiring lock on java.lang.Object@6d9dd520

ここでは、出力から明確にデッドロックの状況を特定できますが、実際のアプリケーションではデッドロックの状況を見つけてデバッグするのは非常に困難です。

Javaでデッドロックを検出する方法

Javaでデッドロックを検出するには、アプリケーションのJavaスレッドダンプを調べる必要があります。前回、VisualVMプロファイラまたはjstackユーティリティを使用してスレッドダンプを生成する方法について説明しました。以下に、上記のプログラムのスレッドダンプが示されています。

2012-12-27 19:08:34
Full thread dump Java HotSpot(TM) 64-Bit Server VM (23.5-b02 mixed mode):

"Attach Listener" daemon prio=5 tid=0x00007fb0a2814000 nid=0x4007 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" prio=5 tid=0x00007fb0a2801000 nid=0x1703 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"t3" prio=5 tid=0x00007fb0a204b000 nid=0x4d07 waiting for monitor entry [0x000000015d971000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f658> (a java.lang.Object)
	- locked <0x000000013df2f678> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"t2" prio=5 tid=0x00007fb0a1073000 nid=0x4207 waiting for monitor entry [0x000000015d209000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f678> (a java.lang.Object)
	- locked <0x000000013df2f668> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"t1" prio=5 tid=0x00007fb0a1072000 nid=0x5503 waiting for monitor entry [0x000000015d86e000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f668> (a java.lang.Object)
	- locked <0x000000013df2f658> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

"Service Thread" daemon prio=5 tid=0x00007fb0a1038000 nid=0x5303 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" daemon prio=5 tid=0x00007fb0a1037000 nid=0x5203 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" daemon prio=5 tid=0x00007fb0a1016000 nid=0x5103 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" daemon prio=5 tid=0x00007fb0a4003000 nid=0x5003 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" daemon prio=5 tid=0x00007fb0a4800000 nid=0x3f03 in Object.wait() [0x000000015d0c0000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000013de75798> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
	- locked <0x000000013de75798> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:177)

"Reference Handler" daemon prio=5 tid=0x00007fb0a4002000 nid=0x3e03 in Object.wait() [0x000000015cfbd000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000013de75320> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:503)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
	- locked <0x000000013de75320> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=5 tid=0x00007fb0a2049800 nid=0x3d03 runnable 

"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fb0a300d800 nid=0x3503 runnable 

"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fb0a2001800 nid=0x3603 runnable 

"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fb0a2003800 nid=0x3703 runnable 

"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fb0a2004000 nid=0x3803 runnable 

"GC task thread#4 (ParallelGC)" prio=5 tid=0x00007fb0a2005000 nid=0x3903 runnable 

"GC task thread#5 (ParallelGC)" prio=5 tid=0x00007fb0a2005800 nid=0x3a03 runnable 

"GC task thread#6 (ParallelGC)" prio=5 tid=0x00007fb0a2006000 nid=0x3b03 runnable 

"GC task thread#7 (ParallelGC)" prio=5 tid=0x00007fb0a2006800 nid=0x3c03 runnable 

"VM Periodic Task Thread" prio=5 tid=0x00007fb0a1015000 nid=0x5403 waiting on condition 

JNI global references: 114


Found one Java-level deadlock:
=============================
"t3":
  waiting to lock monitor 0x00007fb0a1074b08 (object 0x000000013df2f658, a java.lang.Object),
  which is held by "t1"
"t1":
  waiting to lock monitor 0x00007fb0a1010f08 (object 0x000000013df2f668, a java.lang.Object),
  which is held by "t2"
"t2":
  waiting to lock monitor 0x00007fb0a1012360 (object 0x000000013df2f678, a java.lang.Object),
  which is held by "t3"

Java stack information for the threads listed above:
===================================================
"t3":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f658> (a java.lang.Object)
	- locked <0x000000013df2f678> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)
"t1":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f668> (a java.lang.Object)
	- locked <0x000000013df2f658> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)
"t2":
	at com.journaldev.threads.SyncThread.run(ThreadDeadlock.java:41)
	- waiting to lock <0x000000013df2f678> (a java.lang.Object)
	- locked <0x000000013df2f668> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:722)

Found 1 deadlock.

スレッドダンプの出力は、明確にデッドロックの状況と関与するスレッドとリソースを示しています。デッドロックを分析するためには、BLOCKED状態のスレッドとそのロックを待っているリソースを探す必要があります。各リソースは一意のIDを持っており、これを使用してオブジェクトのロックを既に保持しているスレッドを特定できます。たとえば、スレッド「t3」は0x000000013df2f658をロックするのを待っていますが、既にスレッド「t1」がロックしています。デッドロックの状況を分析し、デッドロックの原因となるスレッドを特定したら、デッドロック状況を回避するためにコードを変更する必要があります。

Javaでデッドロックを回避する方法

これらは、ほとんどのデッドロック状況を回避するためのガイドラインです。

  • ネストされたロックを避ける:これはデッドロックの最も一般的な原因であり、すでにロックを保持している場合は他のリソースをロックしないようにしてください。1つのオブジェクトロックで作業している場合、デッドロックの状況にほぼ不可能です。たとえば、以下はネストされたロックのないrun()メソッドの別の実装であり、デッドロックの状況なしでプログラムが正常に実行されます。

        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println(name + " は " + obj1 + " をロックしました");
            synchronized (obj1) {
                System.out.println(name + " は " + obj1 + " のロックを取得しました");
                work();
            }
            System.out.println(name + " は " + obj1 + " のロックを解放しました");
            System.out.println(name + " は " + obj2 + " をロックしました");
            synchronized (obj2) {
                System.out.println(name + " は " + obj2 + " のロックを取得しました");
                work();
            }
            System.out.println(name + " は " + obj2 + " のロックを解放しました");
    
            System.out.println(name + " の実行が終了しました。");
        }
    
  • 必要なものだけをロックする:作業する必要があるリソースに対してのみロックを取得するべきです。たとえば、上記のプログラムでは、完全なオブジェクトリソースをロックしていますが、特定のフィールドにのみ興味がある場合は、その特定のフィールドのみをロックすべきです。

  • 無期限の待機を避ける:2つのスレッドが互いに無期限に終了を待ち続ける場合、デッドロックが発生する可能性があります。スレッドが別のスレッドの終了を待つ必要がある場合は、スレッドの終了を待つ最大時間を指定してjoinを使用するのが最適です。

これでJavaスレッドのデッドロックについての説明は終わりです。

Source:
https://www.digitalocean.com/community/tutorials/deadlock-in-java-example