JAVA中死锁问题排查和预防

在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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容

  • 1、竞态条件: 定义:竞态条件指的是一种特殊的情况,在这种情况下各个执行单元以一种没有逻辑的顺序执行动作,从而导致...
    Hughman阅读 1,287评论 0 7
  • 死锁产生的原因和解锁的方法 产生死锁的四个必要条件: (1) 互斥条件:一个资源每次只能被一个进程使用。 (2) ...
    憩在河岸上的鱼丶阅读 1,480评论 0 4
  • 产生死锁的四个必要条件: (1) 互斥条件:一个资源每次只能被一个进程使用。 (2) 请求与保持条件:一个进程因请...
    像敏锐的狗阅读 975评论 0 0
  • 1. cpu通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一任务。但是,再切换之前会保存上一...
    冰与河豚鱼阅读 664评论 0 0
  • 一扩展javalangThread类二实现javalangRunnable接口三Thread和Runnable的区...
    和帅_db6a阅读 487评论 0 1