死锁导致的线程全部阻塞

在多线程编程中,死锁(Deadlock)是一个常见且棘手的问题。当多个线程互相等待对方释放资源,而没有一个线程能够继续执行时,就会发生死锁,导致所有相关线程被阻塞,程序失去响应。本文将深入探讨死锁的原理、产生条件、诊断方法以及解决方案。

什么是死锁?

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

死锁的经典示例

让我们通过一个简单的Java代码示例来演示死锁:

java
public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();
    
    public static void main(String[] args) {
        // 线程1:先锁resource1,再锁resource2
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("线程1:锁住了resource1");
                
                try {
                    Thread.sleep(100); // 模拟一些操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (resource2) {
                    System.out.println("线程1:锁住了resource2");
                }
            }
        });
        
        // 线程2:先锁resource2,再锁resource1
        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("线程2:锁住了resource2");
                
                try {
                    Thread.sleep(100); // 模拟一些操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (resource1) {
                    System.out.println("线程2:锁住了resource1");
                }
            }
        });
        
        thread1.start();
        thread2.start();
    }
}

运行这段代码,很可能出现以下输出:

text
线程1:锁住了resource1
线程2:锁住了resource2

然后程序就会卡住,两个线程都无法继续执行。

死锁产生的四个必要条件

死锁的发生必须同时满足以下四个条件:

1. 互斥条件

资源在同一时刻只能被一个线程占用。如果资源可以被多个线程共享,就不会发生死锁。

2. 请求与保持条件

线程已经保持至少一个资源,同时又请求其他资源,而该资源被其他线程占用,导致该线程阻塞,但又不释放自己已持有的资源。

3. 不剥夺条件

线程已获得的资源在未使用完之前,不能被其他线程强行剥夺,只能由自己主动释放。

4. 循环等待条件

在发生死锁时,必然存在一个线程-资源的环形链。即线程集合{T0, T1, T2, …, Tn}中,T0等待T1占用的资源,T1等待T2占用的资源,…,Tn等待T0占用的资源。

如何诊断死锁

1. 使用jstack工具

对于Java应用,JDK自带的jstack工具是诊断死锁的有力工具:

bash
# 首先找到Java进程的PID
jps -l

# 使用jstack查看线程堆栈
jstack <PID>

当存在死锁时,jstack的输出中会明确提示:

text
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f8b7c006b00 (object 0x00000007d6a2c9a0, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f8b7c008b00 (object 0x00000007d6a2c9b0, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
    at DeadlockExample.lambda$main$1(DeadlockExample.java:28)
    - waiting to lock <0x00000007d6a2c9a0> (a java.lang.Object)
    - locked <0x00000007d6a2c9b0> (a java.lang.Object)
    at DeadlockExample$$Lambda$2/1831932724.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)
"Thread-0":
    at DeadlockExample.lambda$main$0(DeadlockExample.java:14)
    - waiting to lock <0x00000007d6a2c9b0> (a java.lang.Object)
    - locked <0x00000007d6a2c9a0> (a java.lang.Object)
    at DeadlockExample$$Lambda$1/558638686.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

2. 使用JConsole图形化工具

JConsole是JDK自带的图形化监控工具,可以直观地检测死锁:

  1. 运行 jconsole 命令启动

  2. 连接到目标Java进程

  3. 点击”线程”选项卡

  4. 点击”检测死锁”按钮

JConsole会高亮显示死锁的线程,并展示它们等待的资源关系。

3. 通过代码检测

也可以在代码中主动检测死锁:

java
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
    public static void checkDeadlocks() {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
        
        if (deadlockedThreads != null && deadlockedThreads.length > 0) {
            System.out.println("检测到死锁!涉及以下线程:");
            ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
            for (ThreadInfo threadInfo : threadInfos) {
                System.out.println("线程: " + threadInfo.getThreadName());
                System.out.println("状态: " + threadInfo.getThreadState());
                System.out.println("等待的锁: " + threadInfo.getLockName());
                System.out.println("持有的锁: " + threadInfo.getLockOwnerName());
                System.out.println("---");
            }
        } else {
            System.out.println("未检测到死锁");
        }
    }
}

死锁的预防策略

1. 破坏互斥条件

  • 使用无锁编程技术,如CAS(Compare and Swap)

  • 使用线程安全的并发容器,如ConcurrentHashMap

  • 但有些资源天生就是互斥的,无法完全破坏

2. 破坏请求与保持条件

  • 一次性申请所有资源:要求线程在开始执行前,一次性申请所有需要的资源

java
public class SafeAllocation {
    // 使用一个锁来保护资源分配
    private static final Object allocationLock = new Object();
    
    public void performOperation() {
        synchronized (allocationLock) {
            // 同时获取所有需要的资源
            synchronized (resource1) {
                synchronized (resource2) {
                    // 执行操作
                }
            }
        }
    }
}

3. 破坏不剥夺条件

  • 如果线程申请不到所有资源,就释放已经持有的资源

  • 使用tryLock操作,设置超时时间

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

public class TryLockExample {
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();
    
    public void performOperation() {
        while (true) {
            try {
                // 尝试获取lock1,最多等待100ms
                if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                    try {
                        // 尝试获取lock2,最多等待100ms
                        if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                            try {
                                // 成功获取两个锁,执行操作
                                System.out.println("成功获取两个锁,执行操作");
                                return;
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } finally {
                        // 如果没有获取到lock2,释放lock1
                        lock1.unlock();
                    }
                }
                
                // 没有获取到所有锁,等待一段时间后重试
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}

4. 破坏循环等待条件

  • 锁排序:规定所有线程必须按照相同的顺序获取锁

java
public class LockOrdering {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();
    
    // 定义锁的获取顺序,总是先获取lock1,再获取lock2
    public void method1() {
        synchronized (lock1) {
            synchronized (lock2) {
                // 执行操作
            }
        }
    }
    
    public void method2() {
        synchronized (lock1) {  // 先获取lock1
            synchronized (lock2) {  // 再获取lock2
                // 执行操作
            }
        }
    }
}

死锁的避免策略

1. 使用定时锁

通过tryLock的超时机制,避免线程无限期等待:

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

public class TimeoutLockExample {
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();
    
    public boolean transfer() {
        boolean lock1Acquired = false;
        boolean lock2Acquired = false;
        
        try {
            lock1Acquired = lock1.tryLock(1, TimeUnit.SECONDS);
            if (!lock1Acquired) {
                return false;
            }
            
            // 模拟一些操作
            Thread.sleep(100);
            
            lock2Acquired = lock2.tryLock(1, TimeUnit.SECONDS);
            if (!lock2Acquired) {
                return false;
            }
            
            // 执行实际的操作
            System.out.println("操作成功执行");
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock2Acquired) {
                lock2.unlock();
            }
            if (lock1Acquired) {
                lock1.unlock();
            }
        }
    }
}

2. 使用并发工具类

Java提供了许多高级并发工具,可以有效避免死锁:

  • Semaphore:信号量,控制同时访问资源的线程数

  • CountDownLatch:允许一个或多个线程等待其他线程完成操作

  • CyclicBarrier:允许多个线程互相等待,直到到达某个公共屏障点

  • BlockingQueue:线程安全的队列,可以避免显式的锁操作

java
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private final Semaphore semaphore1 = new Semaphore(1);
    private final Semaphore semaphore2 = new Semaphore(1);
    
    public void safeMethod() {
        try {
            semaphore1.acquire();
            semaphore2.acquire();
            
            // 执行操作
            System.out.println("操作执行中...");
            
            semaphore2.release();
            semaphore1.release();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

实际案例分析

案例:银行转账系统

一个典型的银行转账系统,如果没有正确处理锁,很容易发生死锁:

java
public class BankAccount {
    private final int id;
    private double balance;
    private final Lock lock = new ReentrantLock();
    
    public BankAccount(int id, double balance) {
        this.id = id;
        this.balance = balance;
    }
    
    // 有死锁风险的转账方法
    public void transferDeadlock(BankAccount target, double amount) {
        // 先锁住当前账户
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + 
                " 锁住了账户 " + id);
            
            // 模拟一些操作,增加死锁概率
            Thread.sleep(100);
            
            // 再锁住目标账户
            target.lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + 
                    " 锁住了账户 " + target.id);
                
                if (balance >= amount) {
                    balance -= amount;
                    target.balance += amount;
                    System.out.println("转账成功: " + amount + 
                        " 从账户 " + id + " 到 " + target.id);
                }
            } finally {
                target.lock.unlock();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
    
    // 正确的转账方法:按照账户ID顺序获取锁
    public void transferSafe(BankAccount target, double amount) {
        BankAccount firstLock = this.id < target.id ? this : target;
        BankAccount secondLock = this.id < target.id ? target : this;
        
        firstLock.lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + 
                " 锁住了账户 " + firstLock.id);
            
            Thread.sleep(100);
            
            secondLock.lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + 
                    " 锁住了账户 " + secondLock.id);
                
                if (this.balance >= amount) {
                    this.balance -= amount;
                    target.balance += amount;
                    System.out.println("转账成功: " + amount + 
                        " 从账户 " + this.id + " 到 " + target.id);
                }
            } finally {
                secondLock.lock.unlock();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            firstLock.lock.unlock();
        }
    }
}

总结

死锁是多线程编程中一个严重的问题,会导致线程全部阻塞,系统失去响应。要有效预防和处理死锁,我们需要:

  1. 理解死锁的本质:四个必要条件同时满足

  2. 掌握诊断工具:jstack、JConsole等

  3. 采取预防策略:破坏四个必要条件中的任意一个

  4. 使用高级并发工具:利用Java提供的并发类库

  5. 遵循最佳实践:锁排序、超时机制等

在实际开发中,我们应该在设计阶段就考虑死锁的预防,并在测试阶段充分测试并发场景。同时,保持代码简单清晰,避免复杂的嵌套锁结构,也能大大降低死锁的风险。

记住:预防胜于治疗,在编写多线程代码时,始终要警惕死锁的可能性!

购买须知/免责声明
1.本文部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责。
2.若您需要商业运营或用于其他商业活动,请您购买正版授权并合法使用。
3.如果本站有侵犯、不妥之处的资源,请在网站右边客服联系我们。将会第一时间解决!
4.本站所有内容均由互联网收集整理、网友上传,仅供大家参考、学习,不存在任何商业目的与商业用途。
5.本站提供的所有资源仅供参考学习使用,版权归原著所有,禁止下载本站资源参与商业和非法行为,请在24小时之内自行删除!
6.不保证任何源码框架的完整性。
7.侵权联系邮箱:aliyun6168@gail.com / aliyun666888@gail.com
8.若您最终确认购买,则视为您100%认同并接受以上所述全部内容。

小璐导航资源站 java 死锁导致的线程全部阻塞 https://o789.cn/25100.html

相关文章

猜你喜欢