并发系列之死锁

并发系列之死锁

先看一段产生死锁的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.wyj.jvm.deadLock;

/**
* Created by wyj on 2017/9/4
*/
public class DeadLockDemo {

private static String A = "A";
private static String B = "B";

public static void main(String[] args) {

new DeadLockDemo().deadLock();
}

private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}

输出:

1
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java  ...

就会造成死锁的场景。

这段代码只是演示死锁的场景,在现实中你可能不会写出这样的代码。但是,在一些更为复杂的场景中,你可能不会写出这样的代码。但是,在一些更为复杂的场景中,你可能会遇到这样的问题,比如t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候跑出了异常,没释放掉。

一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程查看哪个线程出现了问题。下面看一下是怎么样的排查过程,输入top命令查看各个进程cpu使用情况,默认按cpu使用率排序:

top

可以看到一共有两个java线程,也就是t1和t2,利用jstack PID命令,dump线程查看到底是哪个线程出现了问题。

jstack1

jstack2

可以看到线程的状态:waiting on condition

线程的调用栈

线程的当前锁住的资源:<0x000000076ac9f420>

在stack information里面完整的告诉了你哪个线程的第几行开始报错了。

看一下线程存在的五种状态

线程七种状态

对于jstack日志,着重关注如下关键信息

DeadLock:表示有死锁

Waiting on condition:等待某个资源或条件发生来唤醒自己。具体需要结合jstacktrace来分析,比如线程正在sleep,或者是线程等待网络的读写。

Blocked:阻塞,在进入同步方法或同步代码块,没有获取到锁时,就会进入阻塞状态。

Waiting on monitor entry 和 in Object.wait():

在多线程的 JAVA程序中,实现线程之间的同步,就要说说 Monitor。 Monitor是 Java中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 Class的锁。每一个对象都有,也仅有一个 monitor。每个 Monitor在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”,而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。 先看 “Entry Set”里面的线程。我们称被 synchronized保护起来的代码段为临界区。当一个线程申请进入临界区时,它就进入了 “Entry Set”队列。

对应的 code就像: synchronized(obj) { ……… }

这时有两种可能性:

该 monitor不被其它线程拥有, Entry Set里面也没有其它等待线程。本线程即成为相应类或者对象的 Monitor的 Owner,执行临界区的代码

该 monitor被其它线程拥有,本线程在 Entry Set队列中等待。

在第一种情况下,线程将处于 “Runnable”的状态,而第二种情况下,线程 DUMP会显示处于 “waiting for monitor entry”。

临界区的设置,是为了保证其内部的代码执行的原子性和完整性。但是因为临界区在任何时间只允许线程串行通过,这 和我们多线程的程序的初衷是相反的。 如果在多线程的程序中,大量使用 synchronized,或者不适当的使用了它,会造成大量线程在临界区的入口等待,造成系统的性能大幅下降。如果在线程 DUMP中发现了这个情况,应该审查源码,改进程序。 现在我们再来看现在线程为什么会进入 “Wait Set”。当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(一般就是被 synchronized 的对象)的 wait() 方法,放弃了 Monitor,进入 “Wait Set”队列。只有当别的线程在该对象上调用了 notify() 或者 notifyAll() , “ Wait Set”队列中线程才得到机会去竞争,但是只有一个线程获得对象的 Monitor,恢复到运行态。在 “Wait Set”中的线程, DUMP中表现为: in Object.wait(),

最后总结一下避免死锁的方法:

1:避免一个线程同时获取多个锁。

2:避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。

3:尝试使用定时锁,使用lock.tryLock(timeout) 来替代使用内部锁机制。

4:对于数据锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。