Java 中的死锁

问题:你是怎么发现死锁并且是如何预防、如何解决的?

死锁的定义:

这里给出一个我对死锁的概念的理解:

多个线程同时被阻塞,并且他们中的一个或多个都在等待某个被占用资源的释放。

再给出一种百度百科上的解释,比较全面,便于交叉理解:

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁产生的必要条件:

  • 互斥 :一个资源每次只能被一个线程占用
  • 不可剥夺:进程对正在被其他进程占用的资源,不能强行剥夺
  • 请求与保持: 一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。

注:只要一个条件不满足,就不会发生死锁,所以避免死锁,或者是解决死锁问题,只需要破坏其中一个必要条件即可。

死锁问题例子:

A 线程要在持有锁a的前提下尝试获取锁b

B 线程要在持有锁b的前提下尝试获取锁a

A B 在完成获取锁的动作之前,都不会放弃自身持有的锁,死锁条件达成,接下来是代码:

class Solution {
  public static void main(String[] args) {
    final Object a = new Object();
    final Object b = new Object();

    Thread threadA =
        new Thread(
            new Runnable() {
              @Override
              public void run() {
                synchronized (a) {
                  System.out.println("threadA in a lock");
                  try {
                    Thread.sleep(1000);
                    synchronized (b) {
                      System.out.println("threadA in b lock");
                    }
                  } catch (InterruptedException e) {
                    // ..
                  }
                }
              }
            });
    Thread threadB =
        new Thread(
            new Runnable() {
              @Override
              public void run() {
                synchronized (b) {
                  System.out.println("threadB in a lock");
                  try {
                    Thread.sleep(1000);
                    synchronized (a) {
                      System.out.println("threadB in b lock");
                    }
                  } catch (InterruptedException e) {
                    // ..
                  }
                }
              }
            });
    threadA.start();
    threadB.start();
  }
}

运行结果:

threadA in a lock

threadB in a lock

产生死锁,程序明显停滞了,没有剩余的输出。

死锁检测:

这里我们可以使用JDK提供的两种检测工具,进行简易的死锁检测

Jstack 命令:

jstack工具可以用于生成Java虚拟机当前的快照,是虚拟机中每一条线程正在执行的方法堆栈的集合。

方法:

1 我们可以通过 jps获取当前任务的进程号

E:\react\spring>jps

10256

20404 Launcher

8036 Jps

8188 Solution

2 可以确认任务的进程号是8188,然后执行jstack查看当前进程的堆栈信息

Found one Java-level deadlock:

"Thread-1":

waiting to lock monitor 0x17677e84 (object 0x073f0dd0, a java.lang.Object), which is held by "Thread-0"

"Thread-0": waiting to lock monitor 0x176798c4 (object 0x073f0dd8, a java.lang.Object),

which is held by "Thread-1"

译为:

hread-1这个进程,正在等待一个锁的释放,这个monitor的地址是 0x17677e84,它正在被Thread-0这个线程持有

Thread-0这个进程,正在等待一个锁的释放,这个monitor的地址是 0x176798c4,它正在被Thread-1这个线程持有。

可以很明显的看出,确实存在死锁。

JConsole工具:

JConsole是JDK自带的监控工具,在jdk/bin目录下就可以找到,它用来连接正在运行的本地或远程JVM,对运行在Java应用程序的资源消耗和性能进行监控,并且会以图表的形式进行展示。并且本地占用的服务器内存很小,使用起来非常方便。

方法:

1 在命令行中敲入 jconsole 选择进程号 8188 进行连接

监控图表:

2 选择线程选项卡,点击检测死锁

如图锁时,使用JConsole的时候,也会显示类似Jstack的信息,发现死锁的线程。

如何规避死锁:

正如之前提到的,避免死锁,或者说解决死锁问题,就需要破坏死锁发生的必要条件。

1 尽量不要去使用锁的嵌套,以确定的顺序获取锁 ,避免可能产生的循环依赖问题。

修改后的代码:

class Solution {
  public static void main(String[] args) {
    final Object a = new Object();
    final Object b = new Object();

    Thread threadA =
        new Thread(
            new Runnable() {
              @Override
              public void run() {
                synchronized (a) {
                  System.out.println("threadA in a lock");
                }
                synchronized (b) {
                  System.out.println("threadA in b lock");
                }
              }
            });
    Thread threadB =
        new Thread(
            new Runnable() {
              @Override
              public void run() {
                synchronized (b) {
                  System.out.println("threadB in a lock");
                }
                synchronized (a) {
                  System.out.println("threadB in b lock");
                }
              }
            });
    threadA.start();
    threadB.start();
  }
}

执行结果:

threadA in a lock

threadA in b lock

threadB in a lock

threadB in b lock

2 超时放弃

可以用ReentrantLock中的 tryLock(long time,TimeUnit unit)方法来方式获取锁,该方法会按照固定时长去等待锁,因此线程可以在获取锁超时之后,主动释放之前已经释放的锁。(内部是一个自旋锁,不断的尝试获取锁,超时之后抛出异常)。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

class Solution {
  public static void main(String[] args) throws InterruptedException {
    ReentrantLock lockA = new ReentrantLock();
    ReentrantLock lockB = new ReentrantLock();
    new Thread(new Runnable() {
      @Override
      public void run() {

        try {
          lockA.lock();
          System.out.println("Thread-2 in Alock");
          Thread.sleep(100);
          lockB.tryLock(1,TimeUnit.SECONDS);
          System.out.println("Thread-2 in Block");
        } catch (InterruptedException e) {
          e.printStackTrace();
        }finally{
          lockB.unlock();
          lockA.lock();
        }
      }
    }).start();
    new Thread(new Runnable() {
      @Override
      public void run() {

        try {
          lockB.lock();
          System.out.println("Thread-1 in Block");
          Thread.sleep(100);
          lockA.lock();
          System.out.println("Thread-1 in Alock");


        } catch (InterruptedException e) {
          e.printStackTrace();
        }finally{
          lockA.unlock();
          lockB.lock();
        }
      }
    }).start();
  }
}

执行结果:

Thread-2 in Alock

Thread-1 in Block

Thread-2 in Block

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457) at Solution$1.run(Solution.java:22) at java.lang.Thread.run(Thread.java:748)

虽然Thread-0 抛出异常,但是Thread-1中的语句全部执行,并没有发生死锁,程序不会阻塞住。

猜你喜欢

转载自blog.csdn.net/Kirito19970409/article/details/86689968