死锁:产生原因和解决方法

本文详细介绍了死锁的四个必要条件:互斥条件、请求与保持条件、不可剥夺条件和循环等待条件,并通过Java代码示例展示了死锁的产生。为解决死锁,提出了避免循环等待、破坏请求与保持条件等方法,并提供了MySQL中的死锁示例及解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

引言

在多线程编程中,死锁是一个常见的问题。当多个线程相互等待彼此持有的资源时,就会发生死锁。死锁会导致线程无法继续执行,程序陷入停滞状态。本篇博客将详细介绍死锁的产生原因和解决方法,帮助您更好地理解和应对死锁问题。

产生原因

死锁的产生通常涉及以下四个必要条件:

  1. 互斥条件(Mutual Exclusion):至少有一个资源必须被一个线程独占使用,即在一段时间内只能由一个线程持有。
  2. 请求与保持条件(Hold and Wait):一个线程在持有某个资源的同时,又请求获取其他线程持有的资源。
  3. 不可剥夺条件(No Preemption):已经分配给一个线程的资源不能被强制性地剥夺,只能由持有者显式地释放。
  4. 循环等待条件(Circular Wait):存在一个线程资源的循环链,每个线程都在等待下一个线程所持有的资源。

当这四个条件同时满足时,就会发生死锁。

示例代码

下面是一个使用Java多线程的示例代码,演示了死锁的产生:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DeadlockExample {
    private static final Logger log = LoggerFactory.getLogger(DeadlockExample.class);

    // 创建两个资源对象
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        // 线程1
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                log.info("Thread 1: Holding resource 1");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource2) {
                    log.info("Thread 1: Holding resource 1 and resource 2");
                }
            }
        });

        // 线程2
        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                log.info("Thread 2: Holding resource 2");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource1) {
                    log.info("Thread 2: Holding resource 1 and resource 2");
                }
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();
    }
}

在上面的示例中,我们创建了两个资源对象resource1resource2。线程1先获取resource1,然后尝试获取resource2;线程2先获取resource2,然后尝试获取resource1。由于两个线程相互等待对方持有的资源,就会发生死锁。

显示线程一直无法结束。

解决方法

为了避免死锁的发生,我们可以采取以下几种解决方法:

  1. 破坏互斥条件:这意味着允许多个线程同时使用资源。然而,对于某些资源(例如,打印机),这是不可能的。

  2. 破坏请求与保持条件:一种方法是要求线程在请求新的资源之前释放所有已经持有的资源。这种方法可能会导致频繁的资源请求和释放,从而降低系统的性能。

  3. 破坏不剥夺条件:这意味着如果一个线程已经持有一些资源,并且其对其他资源的请求不能立即得到满足,那么释放其已经持有的资源。然而,这种方法可能会导致资源的频繁分配和回收,从而降低系统性能。

  4. 破坏循环等待条件:一种常见的方法是对所有的资源进行排序,并要求所有线程都按照这个顺序请求资源。这种方法在实践中被广泛使用,因为它既不需要减少资源的使用,也不需要频繁地分配和回收资源。

  5. 使用超时机制:当一个线程在一定时间内无法获取到所需资源时,就放弃对该资源的请求,释放已经持有的资源,并进行适当的处理。

避免循环等待

为了避免循环等待,我们可以对资源进行排序,并按照固定的顺序获取资源。这样,在请求资源时就不会形成循环链。下面是修改后的示例代码:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DeadlockExample {
    private static final Logger log = LoggerFactory.getLogger(DeadlockExample.class);

    // 创建两个资源对象
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

    public static void main(String[] args) {
        // 线程1
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                log.info("Thread 1: Holding resource 1");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource2) {
                    log.info("Thread 1: Holding resource 1 and resource 2");
                }
            }
        });

        // 线程2
        Thread thread2 = new Thread(() -> {
            synchronized (resource1) {
                log.info("Thread 2: Holding resource 1");

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (resource2) {
                    log.info("Thread 2: Holding resource 1 and resource 2");
                }
            }
        });

        // 启动线程
        thread1.start();
        thread2.start();
    }
}

在修改后的代码中,线程2在请求资源时先获取resource1,而不是直接获取resource2。这样,就避免了循环等待的发生。

破坏请求与保持条件

为了破坏请求与保持条件,我们可以要求一个线程在获取资源之前,先释放它已经持有的资源。下面是修改后的示例代码:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DeadlockExample {
    private static final Logger log = LoggerFactory.getLogger(DeadlockExample.class);

    // 创建两个资源对象
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();

        public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Holding resource 1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //执行完等于释放了resource1 
            System.out.println("Thread 1: Released resource 1");
            synchronized (resource2) {
                System.out.println("Thread 1: Holding resource 2");
            }
           //执行完等于释放了resource2
        });

        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Holding resource 2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Thread 2: Released resource 2");
            synchronized (resource1) {
                System.out.println("Thread 2: Holding resource 1");
            }
        });

        thread1.start();
        thread2.start();
    }
}

在修改后的代码中,线程1在获取resource2之前,先释放已经持有的resource1。这样,就破坏了请求与保持条件。

可以看出程序正常结束,没有死锁。

破坏不可剥夺条件

Java的内置锁机制(synchronized)并不支持强制剥夺已经持有的资源。这是因为Java的锁是不可中断的,一旦一个线程获取了一个对象的锁,其他任何线程都不能强制剥夺这个锁,除非持有锁的线程自己释放这个锁。

在Java中,如果你想实现可中断的锁,你可能需要使用java.util.concurrent包中的Lock接口和相关类,例如ReentrantLock。但是,即使使用这些工具,你也不能强制一个线程释放它已经持有的锁。你只能请求一个线程释放它的锁,然后这个线程可以选择接受这个请求并释放锁,或者忽略这个请求并继续持有锁。

如果想避免死锁,你可能需要使用更高级的并发控制策略,例如避免循环等待条件,或者使用锁顺序来保证线程总是按照一定的顺序获取锁。这些策略都需要在编写代码时非常小心,以避免引入死锁。

MySQL中的死锁示例

下面是一个简单的MySQL死锁示例。假设有两个事务T1和T2,它们分别需要对表A和表B中的某些记录进行操作。

-- 事务T1
START TRANSACTION;
UPDATE A SET value = value + 1 WHERE id = 1;
SLEEP(5);
UPDATE B SET value = value + 1 WHERE id = 1;
COMMIT;

-- 事务T2
START TRANSACTION;
UPDATE B SET value = value + 1 WHERE id = 1;
SLEEP(5);
UPDATE A SET value = value + 1 WHERE id = 1;
COMMIT;

在上述代码中,事务T1首先锁定了表A中id=1的记录,然后等待5秒后尝试锁定表B中id=1的记录。同时,事务T2首先锁定了表B中id=1的记录,然后等待5秒后尝试锁定表A中id=1的记录。这就导致了一个死锁,因为每个事务都在等待另一个事务释放它所需要的资源。

MySQL检测死锁

MySQL提供了一种机制来检测并自动解决死锁。当MySQL检测到死锁时,它会主动回滚其中一个事务,从而让其他事务可以继续执行。你可以通过查询SHOW ENGINE INNODB STATUS命令来查看最近发生的死锁信息。

 避免死锁的MySQL示例

下面的MySQL程序修改了之前的程序,使其按照资源的顺序获取锁,从而避免了死锁。

-- 事务T1
START TRANSACTION;
UPDATE A SET value = value + 1 WHERE id = 1;
SLEEP(5);
UPDATE B SET value = value + 1 WHERE id = 1;
COMMIT;

-- 事务T2
START TRANSACTION;
UPDATE A SET value = value + 1 WHERE id = 1;
SLEEP(5);
UPDATE B SET value = value + 1 WHERE id = 1;
COMMIT;

在这个修改后的程序中,事务T2不再首先锁定表B中id=1的记录,而是和事务T1一样,首先锁定表A中id=1的记录。这样,就破坏了循环等待条件,因此不会发生死锁。

总结

死锁是多线程编程中常见的问题,但我们可以通过合理的设计和编码来避免和解决死锁。本篇博客介绍了死锁的产生原因和常见的解决方法,包括避免循环等待、破坏请求与保持条件、破坏不可剥夺条件和破坏互斥条件。通过理解和应用这些解决方法,我们可以有效地避免死锁问题,提高多线程程序的稳定性和性能。

👉 💐🌸 公众号请关注 "果酱桑", 一起学习,一起进步! 🌸💐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值