美文网首页
JAVA中死锁问题排查和预防

JAVA中死锁问题排查和预防

作者: 猫清扬 | 来源:发表于2020-04-20 19:41 被阅读0次

在Java多线程开发中死锁问题并不少见,当线程间相互等待资源,而又不释放自身的资源时就会导致无穷无尽的等待。

举一个死锁的例子

public class Account {

    private int balance;
    // 转账
    void transfer(Account target, int amt) {
        // 锁定转出账户
        synchronized(this) {
            try {
                Thread.sleep(10);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"lock:"+this+"=>get:"+target);
            // 锁定转入账户
            synchronized(target) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
                System.out.println(Thread.currentThread().getName()+"lock:"+target+"=>get:"+this);

            }
        }
    }


    public static void main(String[] args){
         Account account1 = new Account();
         Account account2 = new Account();
        new Thread(new Runnable() {
            public void run() {
                account1.transfer(account2,10);
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                account2.transfer(account1,10);
            }
        }).start();


    }
}

以上是一个转账的例子,两个账户相互转账,转账时必须要保护自己账户的资源balance和目标的资源balance不会被其他线程修改,就做了加锁。在单线程的情况下时不会有问题的,但是一旦有两个线程同时操作两个账户转账就会出现死锁的问题。两个线程都在等对方先释放资源,会永久地等下去。

如何解决死锁地问题

并发程序出现死锁问题并没什么好地解决办法,一般情况下只能重启应用。因此解决死锁地问题最好的办法就是规避死锁。如何规避呢?有一个叫Coffman的牛人总结出来,只有以下四个条件都发生的时候才会出现死锁。

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用;
  2. 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  3. 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  4. 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

那么也就是说这四个条件只要破坏其中一个就不会发生死锁。首先第一个条件互斥是无法被破坏的,因为我们在多线程环境里加锁就是为了互斥。其他三个条件都是可以被破坏的。

破坏占有且等待条件

要破坏这个条件通常的做法是让一个线程一次性申请所有的资源,在上面的例子上我们可以再建一个单例类AllocatorAllocator来一次性申请两个账户的资源,转账完成后就一起释放资源。

public class Allocator {
    private Allocator(){};
    private static Allocator instance = new Allocator();
    private List<Object> als = new ArrayList<>();
    // 一次性申请所有资源
    synchronized boolean apply(Object from, Object to){
        if(als.contains(from) || als.contains(to)){
            return false;
        } else {
            als.add(from);
            als.add(to);
        }
        return true;
    }
    // 归还资源
    synchronized void free(Object from, Object to){
        als.remove(from);
        als.remove(to);
    }
    public static Allocator getInstance(){
        return instance;
    }
}

class Account {
    // actr 应该为单例
    private Allocator actr = Allocator.getInstance();
    private int balance;
    // 转账
    void transfer(Account target, int amt){
        // 一次性申请转出账户和转入账户,直到成功
        while(!actr.apply(this, target))
        try{
            // 锁定转出账户
            synchronized(this){
                // 锁定转入账户
                synchronized(target){
                    if (this.balance > amt){
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            actr.free(this, target);
        }
    }
    public static void main(String[] args) {
        Account account1 = new Account();
        Account account2 = new Account();
        new Thread(new Runnable() {
            public void run() {
                account1.transfer(account2,10);
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                account2.transfer(account1,10);
            }
        }).start();
    }
}

破坏不可抢占条件

破坏不可强制资源其实就是线程能够主动释放它占有的资源,这一点 synchronized是做不到的。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但是可以使用SDK下的java.util.concurrent 这个包下面提供的 Lock类来解决这个问题。

public class Account {

    private final Lock lock = new ReentrantLock();
    private int balance;
    // 转账
    void transfer(Account target, int amt) throws InterruptedException {
            while(true){
              if(this.lock.tryLock()){ 
                 try {
                     if(target.lock.tryLock()){ 
                         try {
                             if (this.balance > amt) {
                                 this.balance -= amt;
                                 target.balance += amt;
                             }
                         }finally {
                             target.lock.unlock();
                         }

                     }
                 }finally {
                     this.lock.unlock();
                 }
            }
           }
    }
    public static void main(String[] args){
         Account account1 = new Account();
         Account account2 = new Account();
        new Thread(new Runnable() {
            public void run() {
                try {
                    account1.transfer(account2,10);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                try {
                    account2.transfer(account1,10);
                }catch (Exception e){
                    e.printStackTrace();
                }
           
            }
        }).start();
    }
}

破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源。以上账号的例子我们假设每一个账户都有一个id字段,我们用id字段作为排序条件。申请的时候,我们可以按照从小到大的顺序来申请,这样无论有多少个线程进来都会先去拿账户id小的账户资源,这样就不会出现争抢的问题。

public class Account {
    private int id;
    private int balance;

    public Account(int id) {
        this.id = id;
    }

    // 转账
    void transfer(Account target, int amt){
        Account left = this;
        Account right = target;
        if (this.id > target.id) {
            left = target;
            right = this;
        }
        // 锁定序号小的账户
        synchronized(left){
            // 锁定序号大的账户
            synchronized(right){
                if (this.balance > amt){
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }


    public static void main(String[] args){
        Account account1 = new Account(1);
        Account account2 = new Account(2);
        new Thread(new Runnable() {
            public void run() {
                account1.transfer(account2,10);
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                account2.transfer(account1,10);
            }
        }).start();
    }
}

有了以上这三个方案后你可能会有一个疑问,那个方案好呢。从性能上看破坏占有且等待条件这个方案性能最低,它需要同时获得两把锁的使用权才能执行下去,不然就会一直while下去。破坏不可抢占条件这个性能次之,它不用同时获得两个资源的锁,一个资源一个资源的拿,一旦拿不到就会主动释放,但它也会一直while的尝试下去。破坏循环等待条件这个性能最佳,它会提前把资源顺序排序好,避免了发生挣抢的问题。但是破坏循环等待条件的做法对代码产生侵入性,增加了额外的逻辑。所以最优的要看具体的业务性能要求,通常的话破坏不可抢占条件这个方案是一个常用的选择。

线上如何排查死锁的问题

线上死锁问题总是不经意间产生的,跑在tomcat上的应用一旦出现死锁问题就会照成大部分线程阻塞,进而tomcat就会出现假死状态不能正常的服务。所以排查死锁问题是java程序员必备的一个技能。
我们可以使用jconsole这类的工具对java进程进行监控来找到死锁的线程,也可以使用jstack命令来排查。

首先可以用jps来找到当前java的进程号

>jps
14804 Account  //查询出来account这个进程的进程号
17900 Jps

使用jstack命令查询线程运行状态

>jstack 14804 //查看进程下所有线程状态

2020-04-20 19:35:07
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b15 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000002fe9000 nid=0x1b2c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-1" #15 prio=5 os_prio=0 tid=0x0000000020ebb000 nid=0x1e40 waiting for monitor entry [0x0000000021eff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at locktest.Account1.transfer(Account1.java:18)
        - waiting to lock <0x000000076b613c20> (a locktest.Account1)
        - locked <0x000000076b613c30> (a locktest.Account1)
        at locktest.Account1$2.run(Account1.java:39)
        at java.lang.Thread.run(Thread.java:745)

"Thread-0" #14 prio=5 os_prio=0 tid=0x00000000206ab000 nid=0x1578 waiting for monitor entry [0x0000000021dff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at locktest.Account1.transfer(Account1.java:18)
        - waiting to lock <0x000000076b613c30> (a locktest.Account1)
        - locked <0x000000076b613c20> (a locktest.Account1)
        at locktest.Account1$1.run(Account1.java:34)
        at java.lang.Thread.run(Thread.java:745)


"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001e673800 nid=0x24c8 in Object.wait() [0x000000001f9cf000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076af08ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x000000076af08ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001cf90000 nid=0x2acc in Object.wait() [0x000000001f8cf000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076af06b50> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x000000076af06b50> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)



Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001cf927d8 (object 0x000000076b613c20, a locktest.Account1),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001cf93e88 (object 0x000000076b613c30, a locktest.Account1),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at locktest.Account1.transfer(Account1.java:18)
        - waiting to lock <0x000000076b613c20> (a locktest.Account1)
        - locked <0x000000076b613c30> (a locktest.Account1)
        at locktest.Account1$2.run(Account1.java:39)
        at java.lang.Thread.run(Thread.java:745)
"Thread-0":
        at locktest.Account1.transfer(Account1.java:18)
        - waiting to lock <0x000000076b613c30> (a locktest.Account1)
        - locked <0x000000076b613c20> (a locktest.Account1)
        at locktest.Account1$1.run(Account1.java:34)
        at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

可以看到jstack直接就帮我们找到了deadlock

相关文章

  • JAVA中死锁问题排查和预防

    在Java多线程开发中死锁问题并不少见,当线程间相互等待资源,而又不释放自身的资源时就会导致无穷无尽的等待。 举一...

  • Java相关的性能调优方案

    本文介绍了在性能测试过程中Java进程消耗CPU过高的问题排查方法、线程死锁问题排查方法和内存泄露的排查方法 Ja...

  • java死锁问题排查

    首先熟悉一下jstack命令的用法,主要参数有-F -l -m 如下图: 模拟一段死锁的java代码,如下: ``...

  • Java程序死锁,3种方式快速找到死锁代码

    java程序中出现死锁问题,如果不了解排查方法,是束手无策的,今天咱们用三种方法找到死锁问题。 运行下面代码 程序...

  • 如何快速排查死锁?如何避免死锁?

    前言 相信程序员都会碰上这样的问题,Java死锁如何排查?又如何解决呢?那么,何为死锁呢?死锁是指两个或两个以上的...

  • java程序死锁,3种方式快速找到死锁代码

    java程序中出现死锁问题,如果不了解排查方法,是束手无策的,今天咱们用三种方法找到死锁问题。 运行下面代码 pa...

  • java死锁排查

    锁是个非常有用的工具,运用场景非常多,因为其使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会...

  • java死锁排查

    多个线程在竞争锁的过程中彼此之间形成堵塞的现象 排查 jstack查看线程以及堆栈信息 jconsole可视化工具...

  • 2020-04-08数据库死锁问题排查

    在测试同学测试过程中偶然发现日志中出现异常死锁日志如下: 出现问题后,立刻定位日志,排查死锁原因。以下为排查过程,...

  • 死锁问题排查

    首先使用jps查询进程ID然后使用jstack和进程ID查询堆栈日志信息。

网友评论

      本文标题:JAVA中死锁问题排查和预防

      本文链接:https://www.haomeiwen.com/subject/mcefvhtx.html