java嵌套死锁

本文通过具体的代码示例,详细介绍了嵌套管程锁死的现象及其与死锁的区别。文章展示了错误的锁实现可能导致的锁死问题,并提供了一个公平锁的案例进行深入解析。

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

嵌套管程锁死类似于死锁, 下面是一个嵌套管程锁死的场景:

线程1获得A对象的锁。
线程1获得对象B的锁(同时持有对象A的锁)。
线程1决定等待另一个线程的信号再继续。
线程1调用B.wait(),从而释放了B对象上的锁,但仍然持有对象A的锁。


线程2需要同时持有对象A和对象B的锁,才能向线程1发信号。
线程2无法获得对象A上的锁,因为对象A上的锁当前正被线程1持有。
线程2一直被阻塞,等待线程1释放对象A上的锁。


线程1一直阻塞,等待线程2的信号,因此,不会释放对象A上的锁,而线程2需要对象A上的锁才能给线程1发信号……

你可以能会说,这是个空想的场景,好吧,让我们来看看下面这个比较挫的Lock实现:

//lock implementation with nested monitor lockout problem  
public class Lock{  
    protected MonitorObject monitorObject = new MonitorObject();  
    protected boolean isLocked = false;  
  
    public void lock() throws InterruptedException{  
        synchronized(this){  
            while(isLocked){  
                synchronized(this.monitorObject){  
                    this.monitorObject.wait();  
                }  
            }  
            isLocked = true;  
        }  
    }  
  
    public void unlock(){  
        synchronized(this){  
            this.isLocked = false;  
            synchronized(this.monitorObject){  
                this.monitorObject.notify();  
            }  
        }  
    }  
}  


可以看到,lock()方法首先在”this”上同步,然后在monitorObject上同步。如果isLocked等于false,因为线程不会继续调用monitorObject.wait(),那么一切都没有问题 。但是如果isLocked等于true,调用lock()方法的线程会在monitorObject.wait()上阻塞。

这里的问题在于,调用monitorObject.wait()方法只释放了monitorObject上的管程对象,而与”this“关联的管程对象并没有释放。换句话说,这个刚被阻塞的线程仍然持有”this”上的锁。

校对注:如果一个线程持有这种Lock的时候另一个线程执行了lock操作)当一个已经持有这种Lock的线程想调用unlock(),就会在unlock()方法进入synchronized(this)块时阻塞。这会一直阻塞到在lock()方法中等待的线程离开synchronized(this)块。但是,在unlock中isLocked变为false,monitorObject.notify()被执行之后,lock()中等待的线程才会离开synchronized(this)块。

简而言之,在lock方法中等待的线程需要其它线程成功调用unlock方法来退出lock方法,但是,在lock()方法离开外层同步块之前,没有线程能成功执行unlock()。

结果就是,任何调用lock方法或unlock方法的线程都会一直阻塞。这就是嵌套管程锁死。

一个更现实的例子

你可能会说,这么挫的实现方式我怎么可能会做呢?你或许不会在里层的管程对象上调用wait或notify方法,但完全有可能会在外层的this上调。
有很多类似上面例子的情况。例如,如果你准备实现一个公平锁。你可能希望每个线程在它们各自的QueueObject上调用wait(),这样就可以每次唤醒一个线程。

下面是一个比较挫的公平锁实现方式:

//Fair Lock implementation with nested monitor lockout problem  
public class FairLock {  
    private boolean isLocked = false;  
    private Thread lockingThread = null;  
    private List waitingThreads =  
        new ArrayList();  
  
    public void lock() throws InterruptedException{  
        QueueObject queueObject = new QueueObject();  
  
        synchronized(this){  
            waitingThreads.add(queueObject);  
  
            while(isLocked ||  
                waitingThreads.get(0) != queueObject){  
  
                synchronized(queueObject){  
                    try{  
                        queueObject.wait();  
                    }catch(InterruptedException e){  
                        waitingThreads.remove(queueObject);  
                        throw e;  
                    }  
                }  
            }  
            waitingThreads.remove(queueObject);  
            isLocked = true;  
            lockingThread = Thread.currentThread();  
        }  
    }  
  
    public synchronized void unlock(){  
        if(this.lockingThread != Thread.currentThread()){  
            throw new IllegalMonitorStateException(  
                "Calling thread has not locked this lock");  
        }  
        isLocked = false;  
        lockingThread = null;  
        if(waitingThreads.size() > 0){  
            QueueObject queueObject = waitingThread.get(0);  
            synchronized(queueObject){  
                queueObject.notify();  
            }  
        }  
    }  
}  

  co


乍看之下,嗯,很好,但是请注意lock方法是怎么调用queueObject.wait()的,在方法内部有两个synchronized块,一个锁定this,一个嵌在上一个synchronized块内部,它锁定的是局部变量queueObject。
当一个线程调用queueObject.wait()方法的时候,它仅仅释放的是在queueObject对象实例的锁,并没有释放”this”上面的锁。

现在我们还有一个地方需要特别注意, unlock方法被声明成了synchronized,这就相当于一个synchronized(this)块。这就意味着,如果一个线程在lock()中等待,该线程将持有与this关联的管程对象。所有调用unlock()的线程将会一直保持阻塞,等待着前面那个已经获得this锁的线程释放this锁,但这永远也发生不了,因为只有某个线程成功地给lock()中等待的线程发送了信号,this上的锁才会释放,但只有执行unlock()方法才会发送这个信号。

因此,上面的公平锁的实现会导致嵌套管程锁死。更好的公平锁实现方式可以参考Starvation and Fairness

嵌套管程锁死 VS 死锁

嵌套管程锁死与死锁很像:都是线程最后被一直阻塞着互相等待。

但是两者又不完全相同。在死锁中我们已经对死锁有了个大概的解释,死锁通常是因为两个线程获取锁的顺序不一致造成的,线程1锁住A,等待获取B,线程2已经获取了B,再等待获取A。如死锁避免中所说的,死锁可以通过总是以相同的顺序获取锁来避免。
但是发生嵌套管程锁死时锁获取的顺序是一致的。线程1获得A和B,然后释放B,等待线程2的信号。线程2需要同时获得A和B,才能向线程1发送信号。所以,一个线程在等待唤醒,另一个线程在等待想要的锁被释放。

不同点归纳如下:

死锁中,二个线程都在等待对方释放锁。

嵌套管程锁死中,线程1持有锁A,同时等待从线程2发来的信号,线程2需要锁A来发信号给线程1。
<think>嗯,用户现在问的是Java如何预防死锁。好的,首先得回想一下死锁发生的条件,然后根据这些条件来想预防措施。之前用户问过Spring的执行原理,可能有一定的Java基础,但可能对并发编程不太熟悉,所以需要解释得清楚一些。 死锁的四个必要条件我记得是互斥、持有并等待、不可抢占和循环等待。预防的话就是要打破其中一个条件。用户可能需要具体的例子和方法,所以每个条件对应的解决方案要给出。 互斥条件可能不太好打破,因为有些资源必须互斥,比如锁。那其他三个条件应该可以处理。比如持有并等待,可以一次性申请所有资源,这样就不会在等待的时候持有其他资源。这可能需要设计资源分配策略,比如使用Java里的Lock类来尝试获取所有需要的锁,如果获取不到就释放已有的,避免一直等待。 不可抢占条件的话,Java本身锁的机制不支持强制抢占,但可以设计超时机制,比如使用tryLock()方法,超时后释放锁,避免无限等待。这样线程不会一直阻塞,减少了死锁的可能性。 循环等待可以通过按顺序申请资源来预防,比如给所有锁定义一个顺序,线程必须按顺序申请,这样就不会形成循环等待。比如有锁A和锁B,所有线程必须先申请A再申请B,这样就不会有互相等待的情况了。 另外,可能用户需要代码示例来具体说明,比如如何按顺序申请锁,或者使用tryLock的例子。但要注意代码的正确性和简洁性,避免复杂。 还要考虑用户的实际应用场景,比如在数据库操作或者多线程资源竞争时如何应用这些策略。可能还需要提到工具检测,比如用jstack分析线程转储,或者使用可视化工具来监控死锁。 总结的时候,需要简明扼要,强调核心方法,并提醒用户结合实际场景选择合适策略。可能需要分点说明,结构清晰,方便用户理解。</think>### 一、死锁产生的必要条件 要预防死锁,需先理解死锁的 **4 个必要条件**: 1. **互斥**:资源独占(如锁) 2. **持有并等待**:持有资源的同时请求其他资源 3. **不可抢占**:资源不能被强制释放 4. **循环等待**:多个线程形成环形资源依赖 **核心思路**:破坏任意一个条件即可预防死锁 --- ### 二、具体预防策略 #### 1. **破坏 "持有并等待"** **方案**:**一次性申请所有资源** - **实现示例**: ```java Lock lockA = new ReentrantLock(); Lock lockB = new ReentrantLock(); public void method() { while (true) { if (lockA.tryLock()) { try { if (lockB.tryLock()) { try { // 业务逻辑 break; } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } // 随机等待避免活锁 Thread.sleep((long)(Math.random() * 100)); } } ``` #### 2. **破坏 "不可抢占"** **方案**:**设置超时机制** - **实现示例**: ```java if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { try { // 业务逻辑 } finally { lock.unlock(); } } else { // 释放已持有资源并重试 } ``` #### 3. **破坏 "循环等待"** **方案**:**固定资源申请顺序** - **规则**:全局定义资源优先级(如按 `hashCode` 排序) - **示例**: ```java // 定义锁的申请顺序 public void transfer(Account a, Account b, int amount) { Object firstLock = a.hashCode() < b.hashCode() ? a : b; Object secondLock = a.hashCode() < b.hashCode() ? b : a; synchronized (firstLock) { synchronized (secondLock) { // 转账逻辑 } } } ``` #### 4. **破坏 "互斥"** **局限性**:对必须互斥的资源(如锁)无法避免,但可通过以下方式缓解: - 使用无锁数据结构(如 `ConcurrentHashMap`) - 缩小同步代码块范围 --- ### 三、附加防御手段 1. **代码审查**:强制检查锁的嵌套顺序 2. **静态分析工具**:使用 FindBugs/SpotBugs 检测潜在死锁 3. **监控工具**: - `jstack` 生成线程转储 - VisualVM 分析线程依赖 4. **设计原则**: - 避免嵌套锁 - 使用 `ThreadLocal` 减少共享资源竞争 --- ### 四、典型场景解决方案 | 场景 | 方案 | |------|------| | **数据库事务** | 按固定顺序更新记录 | | **多线程转账** | 统一先锁 ID 小的账户 | | **资源池管理** | 使用 `tryLock` 超时重试 | --- ### 总结 Java 预防死锁的核心方法: - **按顺序申请资源** - **设置超时中断** - **减少锁粒度** - **优先使用线程安全类**(如 `ConcurrentHashMap`) 通过组合策略 + 工具检测,可显著降低死锁风险。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值