【Java 开发日记】一个不注意就死锁了,该怎么办呢?

在多线程编程中,死锁是非常常见但也容易被忽视的问题。它通常发生在两个或多个线程互相等待对方释放资源,导致程序无法继续执行。在 Java 中,死锁发生的原因通常是多个线程对多个资源的加锁顺序不当或缺乏适当的锁管理。

1. 什么是死锁?

死锁是指两个或多个线程在执行过程中,因争夺资源而造成一种相互等待的现象,若无外力干涉,它们都将无法向前推进。

死锁的必要条件

死锁的发生通常需要满足以下四个条件:

  1. 互斥条件:至少有一个资源处于非共享模式,一个资源只能由一个线程占用。
  2. 请求与保持条件:一个线程持有至少一个资源,并请求获取其他线程持有的资源。
  3. 不剥夺条件:线程已经获得的资源,在没有使用完之前不能被强行剥夺。
  4. 循环等待条件:多个线程形成一种头尾相接的循环等待资源的关系。

2. 发生死锁的常见场景

一个常见的死锁场景是:两个线程需要两个锁来完成任务,但它们获取锁的顺序不一致。举个简单的例子:

class A {
    synchronized void method1(B b) {
        b.last();
    }
    synchronized void last() {}
}

class B {
    synchronized void method1(A a) {
        a.last();
    }
    synchronized void last() {}
}

public class DeadlockTest {
    public static void main(String[] args) {
        final A a = new A();
        final B b = new B();
        
        Thread t1 = new Thread() {
            public void run() {
                a.method1(b);  // 线程1持有A的锁,等待B的锁
            }
        };
        
        Thread t2 = new Thread() {
            public void run() {
                b.method1(a);  // 线程2持有B的锁,等待A的锁
            }
        };
        
        t1.start();
        t2.start();
    }
}

在上面的代码中:

  • t1 线程调用 a.method1(b),获取 a 对象的锁,然后等待 b 对象的锁。
  • t2 线程调用 b.method1(a),获取 b 对象的锁,然后等待 a 对象的锁。

由于线程 t1 和 t2 正在互相等待对方的锁,所以发生了死锁。

3. 如何避免死锁?

在 Java 中,我们可以采取以下几种策略来避免死锁的发生:

1. 避免嵌套锁

尽量避免在持有一个锁的情况下去获取其他锁。尽量保证每次加锁的顺序一致,避免不同线程对相同资源加锁的顺序不一致。

例如:如果线程1先获取锁A,再获取锁B,线程2应该先获取锁A,再获取锁B,保持锁的顺序一致。

2. 使用定时锁

可以使用 ReentrantLock 代替 synchronized,通过 lock.tryLock() 方法来设置锁的等待时间,避免死锁发生。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class A {
    private final Lock lock = new ReentrantLock();
    
    void method1(B b) {
        try {
            if (lock.tryLock() && b.lock.tryLock()) {
                // 执行任务
            }
        } finally {
            lock.unlock();
            b.lock.unlock();
        }
    }
}

class B {
    private final Lock lock = new ReentrantLock();
    
    void method1(A a) {
        try {
            if (lock.tryLock() && a.lock.tryLock()) {
                // 执行任务
            }
        } finally {
            lock.unlock();
            a.lock.unlock();
        }
    }
}

通过 tryLock 方法,线程可以在规定时间内尝试获取锁。如果无法获取锁,可以选择放弃,避免进入死锁状态。

3. 使用死锁检测工具

在调试过程中,可以使用一些死锁检测工具来辅助定位死锁问题。Java 本身提供了死锁检测工具,通过 ThreadMXBean 可以查看当前 JVM 中的死锁情况。

import java.lang.management.*;

public class DeadlockDetector {
    public static void main(String[] args) {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        
        if (deadlockedThreads != null) {
            System.out.println("Deadlock detected!");
            for (long threadId : deadlockedThreads) {
                ThreadInfo threadInfo = threadMXBean.getThreadInfo(threadId);
                System.out.println("Thread " + threadInfo.getThreadName() + " is deadlocked.");
            }
        } else {
            System.out.println("No deadlock detected.");
        }
    }
}

这段代码可以帮助我们检测当前 JVM 中是否存在死锁线程。如果检测到死锁,可以通过分析死锁信息来定位问题。

4. 使用更高层次的并发控制

Java 还提供了更高级的并发控制机制,如使用 ExecutorServiceSemaphore 或 CountDownLatch 来管理线程间的同步和资源共享,避免手动加锁可能导致的死锁问题。

4. 如何解决已经发生的死锁?

如果你发现程序已经发生了死锁,有几个处理方式:

  • 手动中断线程:可以通过调用线程的 interrupt() 方法来尝试中断处于死锁状态的线程。这不是解决根本问题的好方法,但在紧急情况下可作为一种处理方式。
  • 重启线程或应用:重启线程或整个应用程序也是解决死锁的一种方式,尤其是在无法避免死锁的情况下。

5. 总结与经验

  • 锁顺序要一致:避免因加锁顺序不同而导致死锁。确保多个线程获取锁的顺序是一致的。
  • 使用合适的锁策略:利用 ReentrantLocktryLock 等方法控制锁的获取方式,避免死锁。
  • 定期检查:在开发过程中,尽量使用线程调试工具定期检查死锁,尤其是在多线程复杂的项目中。

死锁是多线程编程中常见的陷阱,需要开发者格外小心。了解死锁的成因,掌握避免死锁的方法,能够让你在开发中少走弯路,提高代码的健壮性。

你在实际开发中遇到过死锁问题吗?