Java中的死锁示例
在多线程环境中,可能会出现这样的情况:一个线程正在等待被另一个线程锁定的资源,该资源又正在等待另一个线程,依此类推,直到此依赖关系循环回到第一个等待线程。因此,所有线程都在互相等待以释放资源以取得任何进一步的进展,并在此过程中永远被阻塞。这种情况在多线程中称为死锁。
死锁示例
为了解释Java中的死锁,我们可以采用两个线程Thread1和Thread2的简单方案,其中Thread1在obj1上持有一个锁,并等待获得对obj2的锁。同时,Thread2在obj2上有一个锁,并等待获取对obj1的锁。在这里,两个线程都在循环循环中被阻塞,其中Thread1等待获取obj2的锁,而Thread2等待获取obj1的锁,从而产生死锁。
Java中死锁的场景
由于未正确使用synced关键字,我们可能会在Java中陷入僵局。可能发生死锁的情况如下。
嵌套同步块,对象顺序相反。
从另一个方法中调用一个同步方法,其中方法不使用同一对象进行同步。
Java死锁示例
第一个示例显示了存在嵌套的同步块且对象顺序相反的情况。
class ThreadA implements Runnable{ private Object obj1; private Object obj2; ThreadA(Object obj1, Object obj2){ this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized(obj1){ System.out.println(Thread.currentThread().getName() + " acquired " + "obj1 lock"); synchronized(obj2){ System.out.println(Thread.currentThread().getName() + " acquired " + "obj2 lock"); } } } } class ThreadB implements Runnable{ private Object obj1; private Object obj2; ThreadB(Object obj1, Object obj2){ this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized(obj2){ System.out.println(Thread.currentThread().getName() + " acquired " + "obj2 lock"); synchronized(obj1){ System.out.println(Thread.currentThread().getName() + " acquired " + "obj1 lock"); } } } } public class DLDemo { public static void main(String[] args) { Object obj1 = new Object(); Object obj2 = new Object(); Thread t1 = new Thread(new ThreadA(obj1, obj2)); Thread t2 = new Thread(new ThreadB(obj1, obj2)); t1.start(); t2.start(); } }
输出:
Thread-0 acquired obj1 lock Thread-1 acquired obj2 lock
在ThreadA的run方法中,同步首先在obj1上完成,然后在obj2上完成。在ThreadB的run方法中,同步是相反的,首先在obj2上完成,然后在obj1上完成。这可能会导致死锁,其中t1获得obj1的锁并等待获取obj2的锁。同时obj2已获取对obj2的锁定,并等待获取对obj1的锁定。
另一个Java死锁示例显示了从另一个调用一个同步方法的情况。
public class DLDemo { public synchronized void method1(DLDemo obj){ System.out.println(Thread.currentThread().getName() + " In Method1"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //Calling another synchronized method obj.method2(this); } public synchronized void method2(DLDemo obj2){ System.out.println("In Method2"); } public static void main(String[] args) { DLDemo obj1 = new DLDemo(); DLDemo obj2 = new DLDemo(); new Thread(new Runnable() { public void run() { obj1.method1(obj2); } }).start(); //Thread 2 new Thread(new Runnable() { public void run() { obj2.method1(obj1); } }).start(); } }
输出:
Thread-0 In Method1 Thread-1 In Method1
在代码中,有两个DLDemo类实例,一个线程使用obj1调用同步方法method1,另一个线程使用obj2调用同步方法method1. 这意味着Thread1持有obj1的锁,而Thread2持有obj2的锁。
在同步方法method1中,有一个对另一个同步方法method2的调用,两个线程都试图用其锁被另一个对象持有的对象调用method2,从而导致死锁。
如何避免Java中的死锁
使用多线程编程可能会发生死锁,并且没有语言支持来防止死锁。我们将必须仔细编写多个线程使用的代码,以避免死锁。在这里,我们将介绍上面显示的方案,并了解如何在这些方案中避免死锁。
1.如前面的场景所示,Java死锁的原因之一是获取锁的方式,如果我们具有嵌套同步,则可以为两个线程以相同顺序而不是相反的顺序获取对象锁。
更改了嵌套同步代码
class ThreadA implements Runnable{ private Object obj1; private Object obj2; ThreadA(Object obj1, Object obj2){ this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized(obj1){ System.out.println(Thread.currentThread().getName() + " acquired " + "obj1 lock"); synchronized(obj2){ System.out.println(Thread.currentThread().getName() + " acquired " + "obj2 lock"); } } } } class ThreadB implements Runnable{ private Object obj1; private Object obj2; ThreadB(Object obj1, Object obj2){ this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized(obj1){ System.out.println(Thread.currentThread().getName() + " acquired " + "obj1 lock"); synchronized(obj2){ System.out.println(Thread.currentThread().getName() + " acquired " + "obj2 lock"); } } } }
输出:
Thread-0 acquired obj1 lock Thread-0 acquired obj2 lock Thread-1 acquired obj1 lock Thread-1 acquired obj2 lock
从输出中可以看到,现在避免了死锁。
2.仅使用synced块将与关键部分代码的同步减至最少也将有助于避免Java中的死锁。
在第二种情况下,可以使用同步块而不是同步整个方法。
public class DLDemo { public void method1(DLDemo obj){ System.out.println(Thread.currentThread().getName() + " In Method1"); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized(this){ //Calling another synchronized method obj.method2(this); } } public void method2(DLDemo obj2){ System.out.println("In Method2"); synchronized(this){ System.out.println("In Method2 synchronized block"); } } public static void main(String[] args) { DLDemo obj1 = new DLDemo(); DLDemo obj2 = new DLDemo(); new Thread(new Runnable() { public void run() { obj1.method1(obj2); } }).start(); //Thread 2 new Thread(new Runnable() { public void run() { obj2.method1(obj1); } }).start(); } }
3通过在Java中使用静态同步。如果使用两个对象实例,则使用两个单独对象的两个线程仍可以使用其单独的对象锁输入同步方法或者块。在这种情况下,静态同步会有所帮助,因为届时将在类级别获取锁。
如何在Java中调试死锁
在Java中检测死锁并不容易,即使日志可能也无济于事。如果我们发现多线程代码的性能不佳,则可能是死锁造成的,最好的办法是获取应用程序的线程转储并进行分析。
我们可以使用jstack实用程序通过提供Java应用程序的pid来获取线程转储。该pid可以通过运行jps命令获得。
作为示例–如果运行由于嵌套同步而在其中创建死锁的程序,则可以使用以下步骤获取线程转储。
1通过使用jps命令,我可以获得Java应用程序的pid。
Jps 5968 7408 DLDemo 13960 Jps
2以pid作为参数运行jstack命令。
Jstack 7408
3获取线程转储并进行分析。这里显示了线程转储的一些相关部分。
"Thread-1" #11 prio=5 os_prio=0 tid=0x000000001b3e1000 nid=0x145c waiting for monitor entry [0x000000001bade000] java.lang.Thread.State: BLOCKED (on object monitor) at com.theitroad.ThreadB.run(DLDemo.java:33) - waiting to lock <0x00000000d5bfaff0> (a java.lang.Object) - locked <0x00000000d5bfb000> (a java.lang.Object) at java.lang.Thread.run(Unknown Source) "Thread-0" #10 prio=5 os_prio=0 tid=0x000000001b3e0000 nid=0x379c waiting for monitor entry [0x000000001b9de000] java.lang.Thread.State: BLOCKED (on object monitor) at com.theitroad.ThreadA.run(DLDemo.java:15) - waiting to lock <0x00000000d5bfb000> (a java.lang.Object) - locked <0x00000000d5bfaff0> (a java.lang.Object) at java.lang.Thread.run(Unknown Source) "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000004d4c800 nid=0x2b34 in Object.wait() [0x000000001acee000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000d5b88ec0> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(Unknown Source) - locked <0x00000000d5b88ec0> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(Unknown Source) at java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source) "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000004d42000 nid=0x6cc in Object.wait() [0x000000001abef000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000000d5b86b68> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Unknown Source) at java.lang.ref.Reference.tryHandlePending(Unknown Source) - locked <0x00000000d5b86b68> (a java.lang.ref.Reference$Lock) at java.lang.ref.Reference$ReferenceHandler.run(Unknown Source) Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x0000000004d47868 (object 0x00000000d5bfaff0, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x0000000004d4a0f8 (object 0x00000000d5bfb000, a java.lang.Object), which is held by "Thread-1" Java stack information for the threads listed above: =================================================== "Thread-1": at com.theitroad.ThreadB.run(DLDemo.java:33) - waiting to lock <0x00000000d5bfaff0> (a java.lang.Object) - locked <0x00000000d5bfb000> (a java.lang.Object) at java.lang.Thread.run(Unknown Source) "Thread-0": at com.theitroad.ThreadA.run(DLDemo.java:15) - waiting to lock <0x00000000d5bfb000> (a java.lang.Object) - locked <0x00000000d5bfaff0> (a java.lang.Object) at java.lang.Thread.run(Unknown Source) Found 1 deadlock.