深入理解与避免Java 死锁

server/2024/9/22 19:30:22/

在 Java 编程中,死锁是一个让人头疼但又至关重要的问题。理解死锁的产生条件以及如何避免死锁,对于编写高效、稳定的多线程程序至关重要。本文将深入探讨 Java 死锁的四个必要条件,并通过具体的例子和解决方案帮助读者更好地理解和避免死锁

一、引言

在多线程编程中,线程之间的协作和资源共享是常见的需求。然而,如果不加以小心处理,就可能会出现死锁的情况。死锁会导致程序无法继续执行,严重影响系统的性能和可靠性。因此,了解死锁的产生条件以及如何避免死锁是每个 Java 开发者都应该掌握的知识。

二、什么是死锁

死锁是指两个或多个线程相互等待对方释放资源,从而导致程序无法继续执行的情况。例如,线程 A 持有资源 X,等待资源 Y;而线程 B 持有资源 Y,等待资源 X。这样,两个线程就陷入了死锁状态,无法继续执行。

三、产生死锁的四个必要条件

(一)互斥条件

  1. 解释
    • 互斥条件是指一个资源每次只能被一个线程使用。这就好比一个房间只能被一个人占用,如果两个人同时想进入这个房间,就必须等待其中一个人先出来。
    • 在 Java 中,很多资源都是互斥的,比如文件、数据库连接、锁等。当一个线程获得了这些资源的锁时,其他线程就必须等待,直到这个线程释放锁。
  2. 例子
    • 假设我们有一个打印机资源,线程 A 正在使用打印机打印文件,这时线程 B 也想使用打印机,但是在 A 使用完之前,B 就无法使用,因为打印机这个资源是互斥的。
    • 又如,在 Java 中使用synchronized关键字来实现线程同步时,被synchronized修饰的方法或代码块就相当于一个互斥资源,同一时间只能被一个线程访问。

(二)请求与保持条件

  1. 解释
    • 请求与保持条件是指一个线程因请求资源而阻塞时,对已获得的资源保持不放。这就像一个人在图书馆里,已经借了几本书(已获得的资源),但又看到了另一本更好的书(新的资源),于是他去请求借阅那本书,但是在请求新资源的时候,他并不愿意放下已经借到的书。
    • 在 Java 中,一个线程可能已经获得了一些资源,然后又去请求新的资源。在等待新资源的过程中,它不会释放已经拥有的资源,这就可能导致死锁
  2. 例子
    • 假设有两个资源 X 和 Y,线程 A 先获得了资源 X,然后又去请求资源 Y。在等待资源 Y 的过程中,线程 A 不会释放资源 X。与此同时,线程 B 先获得了资源 Y,然后又去请求资源 X。这样,两个线程就陷入了死锁状态,因为它们都在等待对方释放资源。
    • 以下是一个用 Java 代码演示请求与保持条件导致死锁的例子:
public class RequestAndHoldDeadlockExample {public static Object resourceX = new Object();public static Object resourceY = new Object();public static void main(String[] args) {Thread threadA = new Thread(() -> {synchronized (resourceX) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resourceY) {System.out.println("Thread A acquired both resources.");}}});Thread threadB = new Thread(() -> {synchronized (resourceY) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resourceX) {System.out.println("Thread B acquired both resources.");}}});threadA.start();threadB.start();}
}

在这个例子中,线程 A 先获得了资源 X,然后去请求资源 Y;线程 B 先获得了资源 Y,然后去请求资源 X。由于两个线程都在等待对方释放资源,所以就发生了死锁

(三)不剥夺条件

  1. 解释
    • 不剥夺条件是指进程已经获得的资源,在未使用完之前,不能强行剥夺。这就像一个人已经拿到了一本书,在他看完这本书之前,别人不能强行把这本书从他手里夺走。
    • 在 Java 中,一个线程已经获得了某个资源的锁,其他线程不能强行剥夺这个锁,只能等待这个线程主动释放锁。
  2. 例子
    • 假设线程 A 获得了资源 X 的锁,正在使用资源 X。这时,线程 B 也想使用资源 X,但是它不能强行剥夺线程 A 对资源 X 的锁,只能等待线程 A 主动释放锁。
    • 以下是一个用 Java 代码演示不剥夺条件导致死锁的例子:
public class NonPreemptionDeadlockExample {public static Object resource = new Object();public static void main(String[] args) {Thread threadA = new Thread(() -> {synchronized (resource) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread A finished using resource.");}});Thread threadB = new Thread(() -> {synchronized (resource) {System.out.println("Thread B acquired resource.");}});threadA.start();threadB.start();}
}

在这个例子中,线程 A 获得了资源的锁,然后进入睡眠状态,模拟正在使用资源。线程 B 试图获得资源的锁,但是由于不剥夺条件,它只能等待线程 A 主动释放锁。如果线程 A 一直不释放锁,那么线程 B 就会一直等待,从而导致死锁

(四)循环等待条件

  1. 解释
    • 循环等待条件是指若干线程之间形成一种头尾相接的循环等待资源关系。这就像几个人围成一圈,每个人都想要他右边的人的东西,同时又拿着自己左边的人想要的东西。这样就形成了一个循环等待的关系,谁也得不到自己想要的东西。
    • 在 Java 中,如果多个线程之间对资源的请求形成了一个循环等待的关系,就可能会发生死锁
  2. 例子
    • 假设有三个资源 A、B、C,线程 1 持有资源 A,等待资源 B;线程 2 持有资源 B,等待资源 C;线程 3 持有资源 C,等待资源 A。这样,三个线程就形成了一个循环等待的关系,从而导致死锁
    • 以下是一个用 Java 代码演示循环等待条件导致死锁的例子:
public class CircularWaitDeadlockExample {public static Object resourceA = new Object();public static Object resourceB = new Object();public static Object resourceC = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (resourceA) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resourceB) {System.out.println("Thread 1 acquired both resources.");}}});Thread thread2 = new Thread(() -> {synchronized (resourceB) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resourceC) {System.out.println("Thread 2 acquired both resources.");}}});Thread thread3 = new Thread(() -> {synchronized (resourceC) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resourceA) {System.out.println("Thread 3 acquired both resources.");}}});thread1.start();thread2.start();thread3.start();}
}

在这个例子中,三个线程分别持有一个资源,然后去请求另一个资源,形成了一个循环等待的关系,从而导致死锁

四、如何避免死锁

(一)破坏互斥条件

  1. 解释
    • 虽然很难完全破坏互斥条件,因为很多资源本身就是天然互斥的,但是在某些特定情况下,可以通过使用资源的共享模式来减少互斥的程度。
    • 例如,对于一些可以同时被多个线程读取的资源,可以使用读写锁来代替普通的互斥锁。这样,多个线程可以同时读取资源,只有在写操作时才需要互斥。
  2. 例子
    • 假设我们有一个共享的计数器资源,多个线程可以同时读取计数器的值,但是只有一个线程可以修改计数器的值。我们可以使用读写锁来实现这个功能:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class AvoidDeadlockByBreakingMutualExclusion {private int counter = 0;private final ReadWriteLock lock = new ReentrantReadWriteLock();public int getCounter() {lock.readLock().lock();try {return counter;} finally {lock.readLock().unlock();}}public void incrementCounter() {lock.writeLock().lock();try {counter++;} finally {lock.writeLock().unlock();}}
}

在这个例子中,多个线程可以同时调用getCounter方法读取计数器的值,因为读操作是共享的。只有当一个线程调用incrementCounter方法修改计数器的值时,才需要互斥。这样就减少了互斥的程度,从而降低了死锁的可能性。

(二)破坏请求与保持条件

  1. 解释
    • 可以要求线程在开始执行之前一次性请求所有需要的资源,而不是在执行过程中逐步请求资源。如果一个线程无法一次性获得所有需要的资源,那么它就应该释放已经获得的资源,然后等待一段时间后再重新尝试。
  2. 例子
    • 假设我们有两个资源 A 和 B,线程需要同时使用这两个资源。我们可以让线程在开始执行之前一次性请求这两个资源,如果无法获得这两个资源,就释放已经获得的资源,然后等待一段时间后再重新尝试:
public class AvoidDeadlockByBreakingRequestAndHold {public static Object resourceA = new Object();public static Object resourceB = new Object();public static void main(String[] args) {Thread thread = new Thread(() -> {while (true) {synchronized (resourceA) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}synchronized (resourceB) {System.out.println("Thread acquired both resources.");break;}}// Release resource A and try again latersynchronized (resourceA) {}}});thread.start();}
}

在这个例子中,线程在获得资源 A 后,如果无法获得资源 B,就会释放资源 A,然后等待一段时间后再重新尝试。这样就避免了请求与保持条件,从而降低了死锁的可能性。

(三)破坏不剥夺条件

  1. 解释
    • 可以设计一种机制,允许在某些情况下强行剥夺一个线程已经获得的资源。但是这种方法比较复杂,并且可能会导致一些问题,所以一般不太常用。
  2. 例子
    • 假设我们有一个资源分配系统,当一个线程长时间持有某个资源而不使用时,系统可以强行剥夺这个资源,并分配给其他需要的线程。为了实现这个功能,我们可以使用一个定时器来检测线程对资源的使用情况,如果一个线程在一定时间内没有使用某个资源,系统就可以强行剥夺这个资源:
import java.util.Timer;
import java.util.TimerTask;public class AvoidDeadlockByBreakingNonPreemption {public static Object resource = new Object();public static void main(String[] args) {Timer timer = new Timer();timer.schedule(new TimerTask() {@Overridepublic void run() {synchronized (resource) {System.out.println("Resource forcibly released.");synchronized (resource) {}}}}, 5000);Thread thread = new Thread(() -> {synchronized (resource) {try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Thread finished using resource.");}});thread.start();}
}

在这个例子中,定时器在 5 秒后会强行剥夺线程对资源的锁。这样就破坏了不剥夺条件,从而降低了死锁的可能性。但是这种方法需要谨慎使用,因为强行剥夺资源可能会导致一些不可预料的问题。

(四)破坏循环等待条件

  1. 解释
    • 可以对资源进行编号,要求线程按照编号顺序请求资源。这样就可以避免循环等待。
  2. 例子
    • 假设我们有三个资源 A、B、C,我们可以给这三个资源编号为 1、2、3。线程在请求资源时,必须按照编号顺序请求资源。例如,线程如果需要同时使用资源 A 和资源 B,那么它必须先请求资源 A,然后再请求资源 B:
public class AvoidDeadlockByBreakingCircularWait {public static Object resourceA = new Object();public static Object resourceB = new Object();public static Object resourceC = new Object();public static void main(String[] args) {Thread thread = new Thread(() -> {int minResource = Math.min(Math.min(resourceA.hashCode(), resourceB.hashCode()), resourceC.hashCode());int maxResource = Math.max(Math.max(resourceA.hashCode(), resourceB.hashCode()), resourceC.hashCode());Object firstResource = minResource == resourceA.hashCode()? resourceA : (minResource == resourceB.hashCode()? resourceB : resourceC);Object secondResource = minResource == resourceA.hashCode()? (resourceB.hashCode() < resourceC.hashCode()? resourceB : resourceC) :(minResource == resourceB.hashCode()? (resourceA.hashCode() < resourceC.hashCode()? resourceA : resourceC) :(resourceA.hashCode() < resourceB.hashCode()? resourceA : resourceB));synchronized (firstResource) {synchronized (secondResource) {System.out.println("Thread acquired both resources.");}}});thread.start();}
}

在这个例子中,线程按照资源的哈希码大小顺序请求资源,避免了循环等待,从而降低了死锁的可能性。

五、总结

死锁是 Java 多线程编程中一个比较复杂但又非常重要的问题。了解死锁的产生条件以及如何避免死锁,对于编写高效、稳定的多线程程序至关重要。本文详细介绍了 Java 死锁的四个必要条件,即互斥条件、请求与保持条件、不剥夺条件和循环等待条件,并通过具体的例子和解决方案帮助读者更好地理解和避免死锁。在实际编程中,我们应该尽量避免死锁的发生,通过合理的资源管理和线程同步机制,确保程序的稳定性和可靠性。


http://www.ppmy.cn/server/120416.html

相关文章

集群聊天服务器项目【C++】(六)MySql数据库

前面已经介绍了网络模块和业务模块&#xff0c;本章介绍数据模块&#xff0c;同样保持模块解耦的特性&#xff0c;即业务模块不能出现数据模块内容&#xff0c;如出现SQL语句&#xff0c;接下来看看怎么实现的。 1.环境安装 第一章已经介绍了MySql安装&#xff0c;但注意需要…

【protobuf】ProtoBuf的学习与使用⸺C++

W...Y的主页 &#x1f60a; 代码仓库分享&#x1f495; 前言&#xff1a;之前我们学习了Linux与windows的protobuf安装&#xff0c;知道protobuf是做序列化操作的应用&#xff0c;今天我们来学习一下protobuf。 目录 ⼀、初识ProtoBuf 步骤1&#xff1a;创建.proto文件 步…

1. ZYNQ 2. MPSOC 3. FPGA 4. Vitis 5. 项目

### 1. 建立Vitis SDK自带的Hello World工程 首先&#xff0c;我们需要在Vitis SDK中创建一个基本的Hello World工程。这是学习FPGA开发和ZYNQ MPSOC平台的重要第一步。Hello World工程的主要目的是验证开发环境的正确性以及熟悉基本的编程流程。 #### 步骤&#xff1a; - 打开…

linux-软件包管理-包管理工具(RedHat/CentOS 系)

Linux 软件包管理&#xff1a;包管理工具&#xff08;RedHat/CentOS 系&#xff09; 一、概述 在 Linux 操作系统中&#xff0c;软件包管理是系统维护的重要部分&#xff0c;它允许用户安装、升级、卸载和查询软件包。不同的 Linux 发行版使用不同的包管理工具。对于 RedHat …

deadlock detected

目录标题 说明&#xff1a;解决方法&#xff1a;预防措施&#xff1a;如何在PostgreSQL中使用pg_locks视图详细查询死锁涉及的事务信息&#xff1f;PostgreSQL中deadlock_timeout参数的具体配置方法和最佳实践是什么&#xff1f; 配置方法最佳实践在PostgreSQL中&#xff0c;如…

Redis存储原理

前言 我们从redis服务谈起&#xff0c;redis是单reactor&#xff0c;命令在redis-server线程处理。还有若干读写IO线程负责IO操作&#xff08;redis6.0之后&#xff0c;Redis之pipeline与事务&#xff09;。此外还有一个内存池线程负责内存管理、一个后台文件线程负责大文件的关…

【Lua坑】Lua协程coroutine无法正常完整执行问题

问题&#xff1a;发现Lua协程执行到一半&#xff0c;突然被掐断了一样等到了设定的时间没有正常执行协程后续代码&#xff01;非必现bug&#xff0c;若发生大概率在高频率使用协程时易触发。 LuaFramework或xLua uLua都自带有协程coroutine&#xff0c;而且基本都使用对象池缓…

9.19工作笔记

怎么做多空对冲 脚本2怎么实现多空对冲的 首先读取factors和periods中的文件&#xff0c;然后read_coin得到结果strategy里面的cal_factor的作用是将所有的因子排名加权得到一个新的因子&#xff0c;这个就是多因子的做法。其中因子权重为factor_list里面的因子的最后一个元素…