死锁,多么可怕的一个词,当生产环境出现此问题时,不仅问题难以排查,而且消耗极大的人力。或许大家会说,怎么可能引起死锁,整个项目我都不涉及多线程,你牛逼,走开,哈哈。大家或许想到了,我们可以使用无锁的方式解决啊,可以,无锁确实可以根除死锁问题,但是当业务逻辑复杂,无锁方式难度太大,我们或许会更加偏向于加锁。现在问题来了,有锁就可能产生死锁。
定义
死锁就是指两个或多个线程执行过程中,相互占用对方需要的资源,而且都不能进行释放,导致线程之间进入无限等待的现象。出现死锁,如果没有外力介入,这种等待将是永久的,对系统的性能造成极大影响。出现死锁时,相关进程不再工作,并且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也是支持远程查看,只是需要远程服务配置端口相关信息,具体用法,大家可以网上搜索,这里不做过多描述。
检测死锁的工具很多,通常这两种方式已经适用,知道如何查看问题,一切都简单。在这里,大家可以发现,我们这里的线程名叫做小李、小王,我们能够很快的发现是哪一块代码出现的问题,所以在实现多线程任务时,最好使用业务名称定义线程,这样可以快速定位问题。
总结
死锁,无非就是多个线程之间资源相互被占用,导致无限阻塞。死锁并不是无中生有的,而是需要四大必要条件。使用无锁和重入锁,可以很好的避免死锁发生。当然,业务复杂,死锁不能完全避免,所以如何查看死锁问题也是重中之重。