阿里面试:什么是死锁?如何解决死锁?

死锁经常被大厂考察,而且实际的开发中,会造成很重要的问题,下面我就来全面详解死锁的原因以及解决办法@mikechen

死锁是什么

死锁是计算机科学、和操作系统领域中一个经典且复杂的问题,它发生在多个进程、或线程因争夺资源而陷入无限等待的状态。

阿里面试:什么是死锁?如何解决死锁?-mikechen

若无外力介入,这些进程将无法继续执行,这就会出现死锁。

 

多线程死锁

死锁在多线程编程中尤其常见,比如使用互斥锁(mutex)时未正确释放。

比如:一组(两个或多个)线程,因争夺资源(比如:库存),而陷入一种互相等待的僵持状态,导致它们都无法继续执行下去。

阿里面试:什么是死锁?如何解决死锁?-mikechen

如下所示:

Thread A: lock(lock1) → lock(lock2)  
Thread B: lock(lock2) → lock(lock1)

两个线程互相等待对方释放锁,形成死锁。

 

数据库死锁

再比如:数据库事务,也是经常发生死锁的地方。

阿里面试:什么是死锁?如何解决死锁?-mikechen

如下所示:

-- 事务 A:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 等待 row 2

-- 事务 B:
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 等待 row 1

事务 A 锁定表 row1,等待 row2,事务 B 锁定 row2,等待 row1,互相阻塞,这样也会出现死锁。

 

如何解决死锁?

死锁:通常是因为线程以不同顺序请求多个锁,造成“循环等待”。

所以,要解决死锁,需要确保所有线程/模块按相同顺序请求资源,破坏“循环等待”条件。

如下所示:

class A {}
class B {}

final A a = new A();
final B b = new B();

Thread t1 = new Thread(() -> {
    synchronized (a) {
        synchronized (b) {
            System.out.println("Thread 1 acquired A then B");
        }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (a) {  // 先锁 A,再锁 B(保持一致)
        synchronized (b) {
            System.out.println("Thread 2 acquired A then B");
        }
    }
});

统一加锁顺序,所有线程获取资源时,遵循同样的顺序。

以及,使用 ReentrantLock 提供的 tryLock() 方法设置超时,避免无限等待。

Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

Thread t1 = () -> {
    try {
        if (lock1.tryLock(1, TimeUnit.SECONDS)) {
            try {
                if (lock2.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        // critical section
                    } finally {
                        lock2.unlock();
                    }
                }
            } finally {
                lock1.unlock();
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

 

数据库死锁解决

如果死锁发生在数据库事务中,解决方法如下:

确保事务按照相同顺序访问资源,破坏“循环等待”条件。

如下所示:

-- 不推荐(死锁风险大):
T1: UPDATE A ...; UPDATE B ...;
T2: UPDATE B ...; UPDATE A ...;

-- 推荐(统一顺序):
T1, T2: UPDATE A ...; UPDATE B ...;

以及,减少事务粒度,拆分长事务。

如下所示:

-- 第一个事务:更新账户余额
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE account_id = 1;
COMMIT;

-- 第二个事务:更新另一账户余额
START TRANSACTION;
UPDATE account SET balance = balance + 100 WHERE account_id = 2;
COMMIT;

-- 第三个事务:更新交易日志
START TRANSACTION;
UPDATE transaction_log SET status = 'completed' WHERE transaction_id = 123;
COMMIT;

每个事务只处理一个简单的更新操作,锁持有时间较短,减少了死锁的概率。

mikechen

mikechen睿哥,10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。

关注「mikechen」公众号,获取更多技术干货!

后台回复架构即可获取《阿里架构师进阶专题全部合集》,后台回复面试即可获取《史上最全阿里Java面试题总结

评论交流
    说说你的看法