1. 线程死锁的原因
死锁:两个或多个线程之间,由于互相持有对方需要的锁,而永久出于阻塞的状态。
会引起死锁的代码示例:
Java代码
-
public static void main(String[] args) throws InterruptedException {
-
String lockA = "lock-A";
-
String lockB = "lock-B";
-
-
Thread t1 = new Thread(()->run(lockA, lockB), "Sample-Thread-A");
-
Thread t2 = new Thread(()->run(lockB, lockA), "Sample-Thread-B");
-
-
t1.start();
-
t2.start();
-
t1.join();
-
t2.join();
-
}
-
-
static void run(String name, String firstLock, String secondLock) {
-
String threadName = Thread.currentThread.getName();
-
synchronized (firstLock) {
-
System.out.println(String.format("%s obtained: %s", threadName, firstLock));
-
try {
-
Thread.sleep(1000);
-
synchronized (secondLock) {
-
System.out.println(String.format("%s obtained: %s", threadName, secondLock));
-
}
-
} catch (InterruptedException e) {
-
// 忽略异常
-
}
-
}
-
}
2. 查找定位死锁
死锁的排查定位往往非常费时费力,不要对“快速解决方案”抱有幻想!
2.1 通过辅助工具定位死锁
jstack、jConsole 等很多工具都可以检查死锁。
在实际应用中,死锁的定位可能会比较复杂。可以通过以下过程排查:
检查区分线程状态
查看等待的目标
对比 Monitor 等持有状态
以上述示例程序为例,程序在运行过程中,我们可以:
# 通过命令 “jstack <pid>” 发现死锁信息:
# 通过 jConsole 中检测到此死锁:
2.2 通过 ThreadMXBean 定位死锁
如果是开发自己的管理工具,也可以在代码中使用 ThreadMXBean 来检测死锁。
注意:对线程进行快照是一个相对重量级的操作,须慎重选择频率和时机!
Java代码
-
ThreadMXBean mBean = ManagementFactory.getThreadMXBean();
-
Runnable dlCheck = () -> {
-
long[] threadIds = mBean.findDeadlockedThreads();
-
if (threadIds != null) {
-
ThreadInfo[] threadInfos = mBean.getThreadInfo(threadIds);
-
System.out.println("Detected deadlock threads:");
-
for (ThreadInfo threadInfo : threadInfos) {
-
System.out.println(threadInfo.getThreadName());
-
}
-
}
-
};
-
-
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
-
scheduler.scheduleAtFixedRate(dlCheck, 5, 10, TimeUnit.SECONDS);
2.3 排查死循环引起的“假死锁”
如果线程进入死循环,它可能被误认为发生了死锁。但是通过前述的方法又发现没有死锁。
这种情况下,死循环线程的 CPU使用率 会飙升。
所以可以查看CPU使用率,找到CPU占用率高的线程,输出线程堆栈信息,排查代码。
以一个Linux环境中的Java程序为例:
a. 通过 top 命令找出CPU占用率较高的进程。
在运维管理合理的实际项目中,我们通常会事先将业务进程的 pid 记录到某个文件,无需额外查找。
配合容器化、微服务的部署,一个容器或一台(虚拟)服务器上不应运行多个业务进程。
而此处 top命令主要是从多个业务进程中找到那个因死循环而CPU占用率超高的进程。
此示例中,目标Java进程的进程号为 24310
b. 通过 ps 命令找出Java进程中CPU占用率较高的线程。
ps -mp 24310 -o THREAD,tid
找到对应线程的线程号为 24311
c. 通过 jstack 输出线程信息,对照上述线程号,找到对应线程堆栈信息,结合代码排查问题。
jstack 24310
jstack 输出的信息中,线程号是以16进制的格式展示的,所以上述线程号 24311 被显示为 0x5ef7
可通过命令 printf "%x\n" 24311 进行转换。
3. 避免死锁
产生死锁的基本要素:
-
互斥条件。锁是独占式,同一时刻只能由单个线程持有。
-
互斥条件(锁)被长期持有。在使用结束之前,线程不会自己释放锁,也不能被其它线程抢占。
-
循环依赖。两个或多个线程之间形成锁的链条环。
根据死锁的基本要素避免死锁:
-
尽量避免使用多个锁。嵌套的 synchronized 和 lock 非常容易出问题。
-
如果必须用多个锁,需仔细设计推演锁的获取顺序。让所有线程按照相同的顺序获取锁,就是一种常见的思路。
-
使用带超时的方法。可以让程序假定可能无法获得锁,并设定锁获取失败时的退出逻辑。