了解死锁之前,我们要先了解线程的状态,或者叫线程的生命周期。
线程的状态主要分为上图中的这几种状态,这里我们需要注意一下几点:
- 初始状态
new出的一个线程对象,注意此时线程并未执行,只有调用start方法后才会执行。 - 运行态
Java中规定将两种合二为一 操作系统分为两种:- 运行中 -->被Cpu分配了时间片
- 就绪 -->等待被Cpu分配了时间片
- 阻塞态
有且仅有调用synchronized关键字且没有拿到锁的情况下算阻塞 wait、sleep等不算,算等待或等待超时
lock显示锁 没有拿到锁 进入等待或等待超时状态,因为Lock底层调用的是LockSupport
阻塞被动进入 等待主动进入
定义
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
造成死锁的原因及解决办法
造成死锁需要以下三个条件,下面我们对这三个条件,进行详细的分析和解释。
-
多个操作者 (M>=2) 争夺多个资源(N>=2) N<=M
为什么需要多个操作者,如果只有一个操作者,还需要加锁吗? 很显然不需要
为什么需要多个资源,如果只有一个资源,谁抢到就是谁的,用完了其他人接着用。
为什么需要N<=M,如果资源数大于操作者数量,操作者可以去争抢没有被争抢的资源。
-
争夺资源顺序不对
public class DieSync {
private static Object objectA = new Object();
private static Object objectB = new Object();
private static void MainDo() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (objectA){
System.out.println(threadName + "get objectA");
Thread.sleep(100);
synchronized (objectB){
System.out.println(threadName + "get objectB");
}
}
}
private static void SonDo() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (objectB){
System.out.println(threadName + "get objectB");
Thread.sleep(100);
synchronized (objectA){
System.out.println(threadName + "get objectA");
}
}
}
private static class SonThread extends Thread{
private String name;
public SonThread(String name) {
this.name = name;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
SonDo();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().setName("主线程");
SonThread sonThread = new SonThread("子线程");
sonThread.start();
MainDo();
}
}
主线程先拿到了A,然后去拿B,发现B被子线程拿了,然后就在这阻塞着
而子线程先拿到了B,然后去拿A,这时候发现A被主线程拿了,然后也阻塞
两者互不相让,你等我,我等你,等到天荒地老,海枯石烂
如果我们将代码改为,主线程和子线程都先拿A在拿B,这个问题就解决了,这也就是解决办法,代码就不演示了,大家可以自行尝试。
-
拿到资源不放手
这点很好理解,就是说你拿到锁了,一直不撒手,占着茅坑不出来了,外面的人自然就拉了裤了,哈哈。
我们下面通过段代码来演示下正确的做法。
public class TryLock {
private static Lock lockA = new ReentrantLock();
private static Lock lockB = new ReentrantLock();
private static void firstToSecond() throws InterruptedException {
String threadNmae = Thread.currentThread().getName();
Random random = new Random();
while (true) {
if (lockA.tryLock()) {
try {
System.out.println(threadNmae + "get lockA");
if (lockB.tryLock()) {
try {
System.out.println(threadNmae + "get lockB");
System.out.println("firstToSecond do work.....");
break;
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
Thread.sleep(random.nextInt(3));
}
}
private static void secondToFirst() throws InterruptedException {
String threadNmae = Thread.currentThread().getName();
Random random = new Random();
while (true) {
if (lockB.tryLock()) {
try {
System.out.println(threadNmae + "get lockB");
if (lockA.tryLock()) {
try {
System.out.println(threadNmae + "get lockA");
System.out.println("secondToFirst do work.....");
break;
} finally {
lockA.unlock();
}
}
} finally {
lockB.unlock();
}
}
Thread.sleep(random.nextInt(3));
}
}
private static class SecondThread extends Thread{
private String name;
public SecondThread(String name) {
this.name = name;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
secondToFirst();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().setName("主线程");
SecondThread secondThread = new SecondThread("子线程");
secondThread.start();
firstToSecond();
}
}
这段代码的逻辑也很简单,就是将内置锁换成了显示锁,显示锁(Lock)可以进行尝试拿锁,中断拿锁等操作。先尝试拿锁,拿到了继续执行,没拿到就继续循环,直到拿到为止。
这里要注意以下两点:
- 当我们使用Lock的时候,一定要记得加try{} finally{}结构,在finally中释放锁,避免我们代码抛出异常,导致锁不释放,造成死锁。
- 我想大家都注意到了这行代码 Thread.sleep(random.nextInt(3));,这行代码的作用就是避免一直尝试拿锁,造成资源浪费,这种现象也叫活锁。打印结果太长了,大家有兴趣可以自己运行代码看一下。
总结
上面我们说造成死锁有三个条件,条件1的造成原因是因为我们的业务需要,没有办法解决,所以我们只能从后两个条件想办法,具体的解决办法,上面代码也给出了示例,主要方法就是
- 调整拿锁顺序
- 用显示锁,尝试去拿锁(注意我们上面提到的注意点)