前言
线程的六种状态(初始,等待,阻塞,运行,就绪和等待),我们都知道,但实际情况下,会出现这六种状态下的,第七种情况死锁,那么死锁是什么,为什么会发生呢?
先了解下死锁的概念
概念
两个线程同一时间持有自身的锁且互相争夺对方的锁,等待对方锁释放,如果没有外力介入下,这两个线程会一直等待下去。
虽然说一直等待,可以说是等待状态,但由于它们互相持有对方所需的锁,此时就是阻塞状态了。
发生死锁条件
1. 互斥条件
2. 持有并等待状态(持有本身锁且等待对方锁)
3. 不可剥夺条件(不可释放本身锁)
4. 环路等待状态(死循环)
栗子
/**
* @author Leo
* @description
* @createDate 2021/8/30 22:22
**/
public class MyLock {
public static Object obj1 = new Object();
public static Object obj2 = new Object();
}
/**
* @author Leo
* @description
* @createDate 2021/8/30 22:21
**/
public class DieLock implements Runnable {
private boolean flag;
public DieLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
synchronized (MyLock.obj1) {
System.out.println(Thread.currentThread().getName() + "....if...obj1..." + flag);
synchronized (MyLock.obj2) {
System.out.println(Thread.currentThread().getName() + ".....if.....obj2....." + flag);
}
}
}
} else {
while (true) {
synchronized (MyLock.obj2) {
System.out.println(Thread.currentThread().getName() + "....else...obj2..." + flag);
synchronized (MyLock.obj1) {
System.out.println(Thread.currentThread().getName() + ".....else.....obj1....." + flag);
}
}
}
}
}
}
/**
* @author Leo
* @description
* @createDate 2021/9/4 21:16
**/
public class DieDemo {
public static void main(String[] args) {
DieLock d1 = new DieLock(true);
DieLock d2 = new DieLock(false);
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
运行结果:
Thread-0....if...obj1...true
Thread-1....else...obj2...false
栗子2
/**
* @author Leo
* @description lock版死锁
* @createDate 2021/9/16 23:13
**/
public class DieLockLock {
public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B
// 创建线程 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lockA.lock(); // 加锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
System.out.println("线程 1:等待获取 B...");
lockB.lock(); // 加锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
}
}
});
t1.start(); // 运行线程
// 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockB.unlock(); // 释放锁
}
}
});
t2.start(); // 运行线程
}
}
日志
线程 1:获取到锁 A!
线程 2:获取到锁 B!
线程 1:等待获取 B...
线程 2:等待获取 A...
从上面栗子可知
1. 互斥条件
t1,t2线程分别持有d1,d2锁,同时又想获取对方的锁,由于锁的互斥性,导致同一个锁同一时间不可被多个线程持有
2. 持有并等待状态、不可剥夺状态
t1,t2线程分别持有d1,d2锁,同时又想获取对方的锁,由于锁的互斥性,同时加上本身自身逻辑问题,又不能释放本身锁,这就导致最后两个线程互相持有本身锁,处于等待状态和不可剥夺状态
3. 环路等待状态
刚才也说过,t1等待d2,t2等待d1,形成死循环
如何排查线程问题
答案:使用java自带的jstack工具,分析项目的线程日志,jstack工具如何使用,该篇文章已介绍过《技术自查番外篇四:Jstack与线程》
如何解决死锁
刚才也提到死锁的四个条件,只要打破任意一条件,解决起来就容易。
大体分为两种方法
1. 资源有序分配法(适用于synchronize和lock)
2. 轮询法(仅适用于lock)
一、资源有序分配法
简单概括:线程之间保证获取锁的顺序是一致。例如:线程A和线程B,以同样的顺序获取锁A和锁B(适用于synchronize和lock,这里以synchronize为实例)
示例代码
/**
* @author Leo
* @description
* @createDate 2021/9/4 21:16
**/
public class DieDemo {
public static void main(String[] args) {
DieLock d1 = new DieLock();
DieLock d2 = new DieLock();
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
/**
* @author Leo
* @description
* @createDate 2021/8/30 22:21
**/
public class DieLock implements Runnable {
@Override
public void run() {
while (true) {
synchronized (MyLock.obj1) {
System.out.println(Thread.currentThread().getName() + "....if...obj1..." );
synchronized (MyLock.obj2) {
System.out.println(Thread.currentThread().getName() + ".....if.....obj2....." );
}
}
}
}
}
/**
* @author Leo
* @description
* @createDate 2021/8/30 22:22
**/
public class MyLock {
public static Object obj1 = new Object();
public static Object obj2 = new Object();
}
日志
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
Thread-0.....if.....obj2.....
Thread-0....if...obj1...
................
二、轮询法
简单概括:轮询获取锁的状态(仅支持lock)
因为lock支持非阻塞式获取锁,所以轮询法仅支持lock
/**
* @author Leo
* @description lock版 trylock 优化版2,轮询
* @createDate 2021/9/16 23:13
**/
public class NoDieLockLock2 {
public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B
// 创建线程 1(使用轮询锁)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 调用轮询锁
pollingLock(lockA, lockB);
}
});
t1.start(); // 运行线程
// 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 线程 2 忘记释放锁资源
// lockB.unlock(); // 释放锁
}
}
});
t2.start(); // 运行线程
}
/**
* 轮询锁
* <p>
*/
public static void pollingLock(Lock lockA, Lock lockB) {
// 轮询次数计数器
while (true) {
if (lockA.tryLock()) { // 尝试获取锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
System.out.println("线程 1:等待获取 B...");
if (lockB.tryLock()) { // 尝试获取锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
System.out.println("线程 1:释放锁 B.");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
System.out.println("线程 1:释放锁 A.");
}
}
// 等待一秒再继续尝试获取锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上述代码由于trylock会一致轮询锁的状态,这样也会造成很大的资源开销,为了解决这问题,我们可以优化下,加个最大的轮询次数
优化1
/**
* @author Leo
* @description lock版 trylock 优化版1,轮询加次数限制
* @createDate 2021/9/16 23:13
**/
public class NoDieLockLock2 {
public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B
// 创建线程 1(使用轮询锁)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 调用轮询锁
pollingLock(lockA, lockB, 3);
}
});
t1.start(); // 运行线程
// 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 线程 2 忘记释放锁资源
// lockB.unlock(); // 释放锁
}
}
});
t2.start(); // 运行线程
}
/**
* 轮询锁
* <p>
* maxCount:最大轮询次数
*/
public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
// 轮询次数计数器
int count = 0;
while (true) {
if (lockA.tryLock()) { // 尝试获取锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
System.out.println("线程 1:等待获取 B...");
if (lockB.tryLock()) { // 尝试获取锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
System.out.println("线程 1:释放锁 B.");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
System.out.println("线程 1:释放锁 A.");
}
}
// 判断是否已经超过最大次数限制
if (count++ > maxCount) {
// 终止循环
System.out.println("轮询锁获取失败,记录日志或执行其他失败策略");
return;
}
// 等待一秒再继续尝试获取锁
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
优化2,轮询+次数+随机时间
/**
* @author Leo
* @description lock版 trylock 优化版4,轮询加次数限制 + 随机睡眠时间
* @createDate 2021/9/16 23:13
**/
public class NoDieLockLock4 {
private static Random rdm = new Random();
public static void main(String[] args) {
Lock lockA = new ReentrantLock(); // 创建锁 A
Lock lockB = new ReentrantLock(); // 创建锁 B
// 创建线程 1(使用轮询锁)
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// 调用轮询锁
pollingLock(lockA, lockB, 3);
}
});
t1.start(); // 运行线程
// 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
lockB.lock(); // 加锁
System.out.println("线程 2:获取到锁 B!");
try {
Thread.sleep(1000);
System.out.println("线程 2:等待获取 A...");
lockA.lock(); // 加锁
try {
System.out.println("线程 2:获取到锁 A!");
} finally {
lockA.unlock(); // 释放锁
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 线程 2 忘记释放锁资源
// lockB.unlock(); // 释放锁
}
}
});
t2.start(); // 运行线程
}
/**
* 轮询锁
* <p>
* maxCount:最大轮询次数
*/
public static void pollingLock(Lock lockA, Lock lockB, int maxCount) {
// 轮询次数计数器
int count = 0;
while (true) {
if (lockA.tryLock()) { // 尝试获取锁
System.out.println("线程 1:获取到锁 A!");
try {
Thread.sleep(1000);
System.out.println("线程 1:等待获取 B...");
if (lockB.tryLock()) { // 尝试获取锁
try {
System.out.println("线程 1:获取到锁 B!");
} finally {
lockB.unlock(); // 释放锁
System.out.println("线程 1:释放锁 B.");
break;
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lockA.unlock(); // 释放锁
System.out.println("线程 1:释放锁 A.");
}
}
// 判断是否已经超过最大次数限制
if (count++ > maxCount) {
// 终止循环
System.out.println("轮询锁获取失败,记录日志或执行其他失败策略");
return;
}
// 等待一秒再继续尝试获取锁
try {
Thread.sleep(300 + rdm.nextInt(8) * 100); // 固定时间 + 随机时间
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}