Java 多线程 - 死锁问题

什么是死锁

百度百科中对于死锁的定义:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

简而言之,当线程1持有资源A,线程2持有资源B。此时线程1想要获取资源B,线程2想要获取资源A。两个线程都想要获取对方手中的资源,自己又不肯让出已有资源,一直僵持不下就形成了死锁。

死锁产生的四个条件

  • 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
  • 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
  • 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
  • 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。

案例

public class DeadLock {
    private OtherService otherService;

    public void setOtherService(OtherService otherService) {
        this.otherService = otherService;
    }

    // DeadLock的实例的锁-资源A
    private final Object LOCK = new Object();

    public void m1() {
        synchronized (LOCK) {
            System.out.println("********m1********");
            otherService.s1();
        }
    }

    public void m2() {
        synchronized (LOCK) {
            System.out.println("********m2********");
        }
    }
}

public class OtherService {

    private DeadLock deadLock;

    public void setDeadLock(DeadLock deadLock) {
        this.deadLock = deadLock;
    }

    // OtherService的实例的锁-资源B
    private final Object LOCK = new Object();

    public void s1() {
        synchronized (LOCK) {
            System.out.println("========s1========");
        }
    }

    public void s2() {
        synchronized (LOCK) {
            System.out.println("========s2========");
            deadLock.m2();
        }
    }
}

public class DeadLockTest {
    public static void main(String[] args) {
        DeadLock deadLock = new DeadLock();
        OtherService otherService = new OtherService();
        deadLock.setOtherService(otherService);
        otherService.setDeadLock(deadLock);

        new Thread(() -> {
            while (true) {
                deadLock.m1();
            }
        }, "T1").start();
        new Thread(() -> {
            while (true) {
                otherService.s2();
            }
        }, "T2").start();
    }
}

上面的案例中,两个线程 T1 和 T2 , 其中 T1 线程调用 DeadLock 的 m1 方法,在 m1 方法内部又调用了 OtherService 的 s1 方法,s1 和 m1 这两个方法都含有用 synchronized 关键字修饰的同步代码块。 T2 线程调用 OtherService 的 s2 方法,在 s2 方法内又调用的 DeadLock 的 m2 方法,同样的,s2 和 m1 这两个方法都含有用 synchronized 关键字修饰的同步代码块。整个程序如图所示:

image-20200610180014070

当 T1 线程执行的时候,m1 方法获取 DeadLock 的 LOCK 锁,并调用 OtherService 的 s1 方法,同时,T2 线程也开始执行,T2 线程获取到 OtherService 的 LOCK 锁,并调用 DeadLock 的 m2 方法,但是由于 m2 的方法的锁此时已经被 T1 线程占有,T2 线程只能等待 T1 线程释放锁,同理,T1 线程也在等待 T2 线程释放锁,于是就形成了死锁。

我们使用 jstack 来观察一下死锁。

image-20200610180513869

首先看到这两个线程互相持有对象的锁,在等待对方释放锁。

image-20200610180620655

jstack 的信息最后也会告诉我们找到一个死锁。

如何避免死锁

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 lock.tryLock(timeout) 来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
posted @ 2020-09-17 14:44  chenxueqiang  阅读(171)  评论(0编辑  收藏  举报