死锁

死锁,多么可怕的一个词,当生产环境出现此问题时,不仅问题难以排查,而且消耗极大的人力。或许大家会说,怎么可能引起死锁,整个项目我都不涉及多线程,你牛逼,走开,哈哈。大家或许想到了,我们可以使用无锁的方式解决啊,可以,无锁确实可以根除死锁问题,但是当业务逻辑复杂,无锁方式难度太大,我们或许会更加偏向于加锁。现在问题来了,有锁就可能产生死锁。

定义

死锁就是指两个或多个线程执行过程中,相互占用对方需要的资源,而且都不能进行释放,导致线程之间进入无限等待的现象。出现死锁,如果没有外力介入,这种等待将是永久的,对系统的性能造成极大影响。出现死锁时,相关进程不再工作,并且CPU占用率为0,死锁的线程不占用CPU。

关于死锁的定义,我相信大家都很了解,但如果叫你写一个死锁的程序,你还会写吗?或许你认为我们都是为了避免死锁,为什么还要写死锁程序呢?首先,你会写死锁程序,在出现死锁之后,你能够更快的发现问题所在。另外,这是面试经常问的啊,能不知道如何写吗?(^_^)

假设现在我们有一个宝箱,现在宝箱被两把锁锁住,同时打开两把锁,就可以得到里面的武林秘籍。来吧,武林秘籍是我的。

public class DeadLock implements Runnable {
    static Object lock1 = new Object();
    static Object lock2 = new Object();
    private Object lock;

    public DeadLock(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        if (lock == lock1) {
            synchronized (lock1) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 哈哈,我终于获得一把锁" + lock1);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + ": 哈哈,武林秘籍是我的了");
                }
            }
        }
        if (lock == lock2) {
            synchronized (lock2) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 哈哈,我终于获得一把锁" + lock2);
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + ": 哈哈,武林秘籍是我的了");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(new DeadLock(lock1), "小王");
        Thread b = new Thread(new DeadLock(lock2), "小李");
        a.start();
        b.start();
        Thread.sleep(5000);
        if(a.isAlive()||a.isAlive()){
            System.out.println("啊,老天啊,第二把锁在哪里啊,");
        }else{
            System.out.println("武林从此多了两个高手");
        }
    }
}

上面代码中,小王获得lock1,小李获得lock2,然后各自找第二把锁,最终都没有打开宝箱。结果如下:

小王: 哈哈,我终于获得一把锁java.lang.Object@3750eeda
小李: 哈哈,我终于获得一把锁java.lang.Object@76843a77
啊,老天啊,第二把锁在哪里啊

怎么办?你们愿意看到他们郁郁而终吗?想个办法吧。咦,有了,如果每个人都先获取lock1,然后才能获取lock2,顺序lock1->lock2,这不就可以了。

修改如下:

Thread a = new Thread(new DeadLock(lock1), "小王");
Thread b = new Thread(new DeadLock(lock1), "小李");
a.start();
b.start();
Thread.sleep(5000);
if(a.isAlive()||a.isAlive()){
    System.out.println("啊,老天啊,第二把锁在哪里啊,");
}else{
    System.out.println("武林从此多了两个高手");
}

现在,小王先获取到锁,然后练成绝世武功,扔掉锁之后,小李捡到了锁,也练成了绝世武功,武林从此多了两个打抱不平的高手,是不是皆大欢喜。结果如下:

小王: 哈哈,我终于获得一把锁java.lang.Object@3750eeda
小王: 哈哈,武林秘籍是我的了
小李: 哈哈,我终于获得一把锁java.lang.Object@3750eeda
小李: 哈哈,武林秘籍是我的了
武林从此多了两个高手

这里,其实我们用到了一个思想,就是每次加锁只针对一个对象,只有当lock1获取之后,才能获取到lock2,按照这样的顺序,就不会出现发生死锁。这种方式使用场景较多,例如,银行转账问题,A给B转账,需要锁定A、B两个账户,如果现在A、B相互给对方转账,就有可能出现死锁问题。这时,我们可以这样做,根据账户特有的字段计算hashcode,然后使用hashcode值比较加锁,要么先锁A账户再锁B账户,要么先锁B账户再锁A账户,这样就可以有效的避免死锁问题。

死锁条件

在多线程中,错误的加锁可能导致死锁产生,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。也就是说,如果不具备其中之一,死锁也就不会存在。

互斥条件:指某个资源在一段时间内只能被一个进程/线程使用。如果此时还有其它进程/线程请求资源,则请求者只能等待,直至占有资源的进程/线程释放。

请求和保持条件:指进程/线程已经拥有至少一个资源,但又想获取另一个资源,而该资源已被其它进程/线程占有,此时请求进程/线程阻塞,但又对自己已获得的其它资源保持不放。

不剥夺条件:指进程/线程已获得的资源,未使用完毕,不能被抢夺,只能主动释放。

环路等待条件:指在发生死锁时,必然存在一个进程/线程——资源的环形链,即进程/线程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

死锁避免

想要真正的避免死锁,我们可以使用无锁的方式,也可以使用重入锁。无锁的方式不谈,既然无锁肯定没有死锁问题。重入锁怎么解决死锁问题呢?大家如果看过我前面Lock的学习与使用,就应该知道我介绍过Lock中的两个方法:

lockInterruptibly():当前线程未被中断,则获取锁定,如果已被中断则抛出异常。

tryLock():限时等待锁。不带参,如果线程没有获得锁,则立即返回false,否则,返回true。方法带时间参数,则表示限时等待,超过时间未获得锁,则则返回false,否则返回true。

使用这两个方法,可以很好地避免死锁出现。如何避免,下面我们使用这两个方法改造一下上面的示例:

public class DeadLock implements Runnable {
    static ReentrantLock lock1 = new ReentrantLock();
    static ReentrantLock lock2 = new ReentrantLock();
    private Object lock;
    public DeadLock(Object lock) {
        this.lock = lock;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(new DeadLock(lock1), "小王");
        Thread b = new Thread(new DeadLock(lock2), "小李");
        a.start();
        b.start();
        Thread.sleep(5000);
        if (a.isAlive() || b.isAlive()) {
           a.interrupt();
        } else {
            System.out.println("武林从此多了两个高手");
        }
    }

    @Override
    public void run() {
        if (lock == lock1) {
            try {
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + ": 哈哈,我终于获得一把锁" + lock1);
                Thread.sleep(1000);
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + ": 哈哈,武林秘籍是我的了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
              if(lock1.isHeldByCurrentThread()){
                  lock1.unlock();
              }
              if(lock2.isHeldByCurrentThread()){
                  lock2.unlock();
              }
            }
        }
        if (lock == lock2) {
            try {
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + ": 哈哈,我终于获得一把锁" + lock2);
                Thread.sleep(1000);
                lock1.lockInterruptibly();
                System.out.println(Thread.currentThread().getName() + ": 哈哈,武林秘籍是我的了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                if(lock2.isHeldByCurrentThread()){
                    lock2.unlock();
                }
                if(lock1.isHeldByCurrentThread()){
                    lock1.unlock();
                }
            }
        }
    }
}

上述代码中,我们取消使用synchronized,改为ReentrantLock重入锁。多次运行,大家看看还会不会出现死锁。测试结果如下:

小王: 哈哈,我终于获得一把锁java.util.concurrent.locks.ReentrantLock@3295296d[Locked by thread 小王]
小李: 哈哈,我终于获得一把锁java.util.concurrent.locks.ReentrantLock@44e71438[Locked by thread 小李]
小李: 哈哈,武林秘籍是我的了
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
	at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
	at com.release.util.container.DeadLock.run(DeadLock.java:36)
	at java.lang.Thread.run(Thread.java:748)

大家可以发现,在5s之后线程并没有死锁,小李成功获取到武林秘籍,小王自动中断,抛出中断异常,放弃持有锁。从输出结果可以看出,ReentrantLock支持响应中断,适当的使用该方式,可以避免死锁发生。大家现在是不是想试一试synchronized是否也支持中断呢?可以试一试,但是不支持,哈哈,答案明确唯一。

关于tryLock()方法,大家可以这样使用:

public void run() {
    if (lock == lock1) {
        try {
            if (lock1.tryLock()) {
                System.out.println(Thread.currentThread().getName() + ": 哈哈,我终于获得一把锁" + lock1);
                Thread.sleep(1000);
                if (lock2.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + ": 哈哈,武林秘籍是我的了");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
        }
    }
    if (lock == lock2) {
        try {
            if (lock2.tryLock()) {
                System.out.println(Thread.currentThread().getName() + ": 哈哈,我终于获得一把锁" + lock2);
                Thread.sleep(1000);
                if (lock1.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + ": 哈哈,武林秘籍是我的了");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
        }
    }
}

大家可以发现,无论执行多少次,都不会出现死锁,这是因为tryLock()无参方式会直接返回true/false,不会进行阻塞,因此也就不会发生死锁。关于有参方式,大家可以自行尝试。

死锁检测

在出现死锁时,能够快速定位问题,相信你会让你的同事或者领导刮目相看。下面简单介绍两种方式:

1.通过jstack查看死锁

 首先,运行第一个示例。然后使用jps获取java程序id。如下:

H:\pack>jps
12388
13508 DeadLock
4692 Jps
9380 Launcher
6744 RemoteMavenServer

            然后使用 jstack+进程id 获取进程当前线程执行情况。我们输出到文件中查看,如下:

 H:\pack>jstack 13508 >> 123.txt

             打开123.txt,部分信息如下:

Found one Java-level deadlock:
=============================
"小李":
  waiting to lock monitor 0x0000000019653038 (object 0x00000000d73f96e0, a java.lang.Object),
  which is held by "小王"
"小王":
  waiting to lock monitor 0x00000000175933e8 (object 0x00000000d73f96f0, a java.lang.Object),
  which is held by "小李"

Java stack information for the threads listed above:
===================================================
"小李":
	at com.release.util.container.DeadLock.run(DeadLock.java:49)
	- waiting to lock <0x00000000d73f96e0> (a java.lang.Object)
	- locked <0x00000000d73f96f0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)
"小王":
	at com.release.util.container.DeadLock.run(DeadLock.java:36)
	- waiting to lock <0x00000000d73f96f0> (a java.lang.Object)
	- locked <0x00000000d73f96e0> (a java.lang.Object)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

该信息通常出现在末尾,从信息末尾中,我们了解到当前程序中出现了一个死锁,该死锁是如何发生的呢?我们来分析一下:Found one Java-level deadlock 发现一个死锁信息,信息包含:小李等待获取锁0x0000000019653038,但是该锁被小王持有,该锁是一个对象(object 0x00000000d73f96e0, a java.lang.Object)。另外小王等待获取锁0x00000000175933e8,但是该锁被小李持有,该锁是一个对象(object 0x00000000d73f96f0, a java.lang.Object)。Java stack information for the threads listed above java 关于线程堆信息,从该信息中,我们可以发现小李,小王互相持有对方所需要的锁并都不释放,因此发生了死锁,并且信息中也指出了出现死锁的行数。根据这些信息,我相信大家能够很快的解决问题。但是有时候,线程堆栈信息特别多,如果使用这种方式,不能很快的找出问题所在。这时,大家可以使用jconsole工具。

2.jconsole监控死锁

首先在jdk安装bin目录下找到jconsole.exe工具,然后双击运行。弹出以下页面:

 

在该页面中,找到我们正在运行的程序DeadLock,然后连接查看。关于jconsole的其它用法暂且不谈,这里主要看线程问题,然后直接点击线程栏,出现如下:

 

大家可以发现,下面有一个检测死锁的按钮,点击一下,如下:

检测到当前程序出现死锁的两个线程,左方显示出现死锁的线程列表,点击线程名,右边出现死锁的具体原因。大家可以看出该信息比我们通过jstack查看更加直观,所以通常建议使用该方法。但是,更多的时候,我们的项目是部署在远程服务器上的,当然jconsole也是支持远程查看,只是需要远程服务配置端口相关信息,具体用法,大家可以网上搜索,这里不做过多描述。

检测死锁的工具很多,通常这两种方式已经适用,知道如何查看问题,一切都简单。在这里,大家可以发现,我们这里的线程名叫做小李、小王,我们能够很快的发现是哪一块代码出现的问题,所以在实现多线程任务时,最好使用业务名称定义线程,这样可以快速定位问题。

总结

死锁,无非就是多个线程之间资源相互被占用,导致无限阻塞。死锁并不是无中生有的,而是需要四大必要条件。使用无锁和重入锁,可以很好的避免死锁发生。当然,业务复杂,死锁不能完全避免,所以如何查看死锁问题也是重中之重。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值