零基础掌握Java多线程(五):死锁

 一、死锁的定义

要想解除阻塞,需要往下执行才可以;要想往下执行,就需要等待前一个锁释放。

构成死锁的四个条件:

(1)互斥

(2)持有且等待

(3)不可强占

(4)循环等待

场景互斥持有且等待不可剥夺循环等待是否死锁
一个线程重复加锁仅不可重入锁时可能
两个线程交叉争锁是(典型死锁)
N个线程环形争锁是(如哲学家就餐问题)

情况1:一线程一锁

一个线程,一把锁,连续加锁两次。

情况2:两线程两锁

两个线程,两把锁,每个线程获取到一把锁之后,尝试获取对方的锁。

        举例:小明和小红各自握着一把钥匙(互斥资源),但需要两把钥匙同时插入才能打开宝箱。小明说:"你先给我钥匙,我才给你";小红说:"你先给我钥匙,我才给你"。结果两人都死死握着自己的钥匙不放,永远打不开宝箱。。

代码示例:

public class DeadLockDemo1 {
    public static void main(String[] args) throws InterruptedException{
        // 创建两把"钥匙"(锁对象),类比小明和小红各自持有的钥匙
        Object locker1 = new Object(); // 第一把锁(小明的钥匙)
        Object locker2 = new Object(); // 第二把锁(小红的钥匙)

        // 线程1(小明):先获取locker1,再尝试获取locker2
        Thread t1 = new Thread(()->{
           synchronized (locker1){ // 小明拿到了自己的钥匙(locker1)
               try {
                   Thread.sleep(1000); // 模拟操作耗时(小明思考1秒钟)
               }catch (InterruptedException e){
                   throw new RuntimeException(e);
               }

               // 小明现在需要小红的钥匙(locker2)才能打开宝箱
               synchronized (locker2){
                   System.out.println("t1线程两个锁都取到"); // 如果拿到两把钥匙,就能打开宝箱
               }
           }
        });

        // 线程2(小红):先获取locker2,再尝试获取locker1
        Thread t2 = new Thread(()->{
           synchronized (locker2){ // 小红拿到了自己的钥匙(locker2)
               try {
                   Thread.sleep(1000); // 模拟操作耗时(小红思考1秒钟)
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }

               // 小红现在需要小明的钥匙(locker1)才能打开宝箱
               synchronized (locker1){
                   System.out.println("t2线程两个锁都取到"); // 如果拿到两把钥匙,就能打开宝箱
               }
           }
        });
        
        // 启动两个线程(小明和小红开始行动)
        t1.start();
        t2.start();
        
        // 等待两个线程结束(实际上会因为死锁而无法结束)
        t1.join();
        t2.join();
    }
}

注:如果不加 sleep, 很可能 t1线程一次就把 locker1 和 locker2 都拿到了。这个时候,t2线程还没开始,无法构成死锁。

情况3:N线程M锁

N个线程M把锁。

        举例:哲学家就餐问题。五根筷子,需要一双筷子才可以就餐。(所以四个人及以下是不会出现死锁的)五个人,每个人都拿起同一侧的一根筷子,而每个人要吃完饭才会释放筷子,所以就会导致大家都没饭吃。

二、可重入锁(Reentrant Lock)

可重入锁(Reentrant Lock):其核心机制正是为了解决线程在持有锁时重复获取同一把锁可能导致的死锁问题(即情况1:一线程一锁)。

为解决这个问题,synchronized就引入了可重入的概念,当某个线程对一个锁,加锁成功之后,后续该线程再次针对同一个线程一把锁连续加多次锁,不会阻塞也不会产生死锁。

1.synchronized实现可重入性的方法

实现可重入的核心原理:“计数器 + 线程ID绑定

​​锁计数器​​:每个锁对象内部维护一个计数器(status)。线程首次获取锁时,计数器加 1;同一线程每次重复获取锁时,计数器继续递增。释放锁时计数器递减,归零时锁完全释放。•

​​线程 ID 检查​​:JVM 会检查当前请求锁的线程是否为锁的持有者。若是,则允许重入;否则线程进入阻塞队列。

2.自己实现一个可重入锁

(1)在锁内部记录当前是哪个线程持有的锁,后续每次加锁,都进行判定。

(2)通过计数器,记录当前加锁的次数,从而确定何时真正进行解锁。

public class ReentrantLockDemo {
    private Thread owner = null; //记录当前持有锁的线程
    private int holdCount = 0; //重入次数计数器

    public synchronized void lock(){
        Thread currentThread = Thread.currentThread();

        //如果当前线程持有锁,但不是当前请求的线程,则当前线程需要等待
        while (holdCount > 0 && owner != currentThread){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();//处理中断异常
            }
        }

        //如果当前没有线程持有锁,或者持有锁就是当前线程,则成功获取锁
        owner = currentThread;
        holdCount++;
    }

    public synchronized void unlock(){
        Thread currentThread =Thread.currentThread();

        //只有锁的持有者才能释放锁
        if(owner != currentThread){
            throw new IllegalStateException("Attempt to unlock a lock not owned by the current thread");
        }
        holdCount--;

        //当计数器归零,表示锁被完全释放,此时唤醒其他线程
        if(holdCount == 0){
            owner =null;
            notify();
        }
    }
}

3.不可重入锁导致的死锁

由不可重入锁直接导致的死锁,绝大多数情况都源于同一线程对同一把锁的重复加锁(即情况1:一线程一锁)。

如果锁不可重入,可能会在看似合理的地方产生死锁,比如以下场景

public class DeadlockDemo2 {
    public void outerMethod(){
        synchronized (this){  //第一次获取锁
            innerMethod();  //在锁尚未释放时,调用另一个需要同一把锁的方法
        }
    }

    public void innerMethod(){
        synchronized (this){  //尝试再次获取同一锁
            //可执行一些操作
        }
    }
}

线程A调用outerMethod(),成功获取锁对象。

在outerMerhod内部,它调用innerMethod()。

innerMethod也在尝试获取同一个对象锁。

如果锁是不可重入的,那么线程A在innerMethod中的获取操作就会失败。因为锁已经被占用(尽管占用着是线程A自己),线程A会被迫等待锁被释放。

4.与显式锁(如 ReentrantLock)的对比​​

synchronized的可重入性是​​隐式​​的,由 JVM 自动管理。

ReentrantLock同样支持可重入,但需手动调用 lock()和 unlock(),并同样依赖计数器机制

三、破坏死锁的关键​​

1.破坏死锁的方法

破坏死锁的关键是破坏其产生的四个条件

(1)对于互斥无法破坏

(2)对于持有且等待,一次性申请所有所需资源​​。线程在开始执行前,就申请其运行所需的所有资源,如果无法同时满足,则暂时不持有任何资源,从而避免“边持有边等待”的局面

(3)对于不可强占,允许强制剥夺资源。​

(4)对于循环等待,规定统一的资源管理顺序。

2.银行家算法的简单了解

银行家算法是一个经典的​​死锁避免​​算法,由艾兹格·迪杰斯特拉提出。它之所以叫“银行家算法”,是因为其思想类似于银行家向客户贷款的过程:银行家需要确保在收到所有客户的贷款请求时,他手头的资金始终能够满足至少一位客户的最高贷款需求,从而保证银行系统能够安全运行(即不会发生死锁)。

银行家算法是一个“先检查,后分配”的死锁避免算法,它在分配资源前会模拟一次分配结果,只有确保系统不会进入死锁状态时,才真正执行分配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值