深入理解与避免Java 死锁

news2024/9/23 17:25:28

在 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 later
                synchronized (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() {
            @Override
            public 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.coloradmin.cn/o/2158072.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

idea上使用tomcat运行web项目

idea上使用tomcat运行web项目 首先需要安装tomcat&#xff0c;可以查看我上一篇文章 如果你已经配置tomcat&#xff0c;这里不需要改动。如果未配置&#xff0c;需要在这里选择你tomcat目录 修改下面的上下文&#xff0c;也就是网页上访问时http://localhost:8080/后面拼…

算数运算之短路

c语言的表达式是从左往右求值的&#xff0c;我们在面对&&&#xff08;按位与&#xff09;和||&#xff08;按位或&#xff09;我们要根据与还有或的特性在判断是否能够使句子能够运行下去&#xff0c;就举个简单的例子 if(month 12 || month1 || month 2) { printf…

LeetCode 2374.边积分最高的节点:模拟

【LetMeFly】2374.边积分最高的节点&#xff1a;模拟 力扣题目链接&#xff1a;https://leetcode.cn/problems/node-with-highest-edge-score/ 给你一个有向图&#xff0c;图中有 n 个节点&#xff0c;节点编号从 0 到 n - 1 &#xff0c;其中每个节点都 恰有一条 出边。 图…

英伟达开源 NVLM 1.0 引领多模态 AI 变革

新闻 NVLM 1.0 是由英伟达&#xff08;Nvidia&#xff09;最新推出的一系列前沿级别的多模态大型语言模型&#xff08;MLLM&#xff09;&#xff0c;这些模型在视觉-语言任务上取得了与领先专有模型&#xff08;例如 GPT-4o&#xff09;和开放访问模型&#xff08;例如 Llama 3…

[干货] [非基础警告] Unity 发布-订阅模式下的事件中心设计

本文师承于唐老师&#xff0c;但是修改了一些代码&#xff0c;采用更加方便理解的方式设计 1.什么是事件中心 2.可以比喻成冒险者工会的任务板子 任务板子上面有发布任务和任务完成两种基础情况 2.1 发布任务 来个冒险者发布一个任务&#xff0c;就执行Pulishtask方法 这…

【软件工程】状态转换图 其他图形工具

状态转换图 一、定义 二、符号表示 其他图形工具 一、层次方框图 二、Warmer图 三、IPO图 例题 选择题

【k8s】:DevOps 模式详解

1.什么是DevOps模式&#xff1f; DevOps 是当下非常火爆的一个概念&#xff0c;受到了很多互联网巨头的推崇。那么什么是 DevOps&#xff1f;它的全称是&#xff1a;集成开发与运维。至于它到底是干什么用的&#xff0c;为什么现在这么火爆&#xff0c;还得从源头说起。 1.1 …

无线感知会议系列【4】【基于WiFi和4G/5G的非接触无线感知:挑战、理论和应用-2】

前言&#xff1a; 本篇重点分享一下该论文 《Human Respiration Detection with Commodity Wifi Devices: Do User Location and Body Orientation Matter》 接 2020年北京智源大会 张大庆老师的一个报告 参考&#xff1a; https://blog.csdn.net/chengxf2/article/detai…

17.2 ksm源码讲解

本节重点介绍 : k8s资源对象的 buildStores构造函数注入MetricFamiliesk8s client-go 之 Reflector listAndWatch 方法watchHandler 监听更新&#xff0c;调用add等action 架构图总结 项目地址 地址 go get go get -v -d k8s.io/kube-state-metrics/v2v2.1.1源码分析 m…

uniapp微信小程序用户授权方法

效果 步骤 1&#xff09;div标签 <button type"primary" class"btn-login" click"getUserInfo">一键登录</button>2&#xff09;js方法 methods: {getUserInfo() {console.log("aaaa")uni.getUserProfile({desc: Wexin, …

Python画笔案例-056 绘制正方形金字塔

1、绘制正方形金字塔 通过 python 的turtle 库绘制 正方形金字塔,如下图: 2、实现代码 绘制正方形金字塔,以下为实现代码: """正方形金字塔.py """ import turtledef draw_square(length):for _ in

【Linux】yum、vim、gcc使用(超详细)

目录 yum 安装软件 卸载软件 查看安装包 安装一下好玩的命令 vim vim基本操作 模式切换 命令集 vim批量注释 vim配置 gcc 函数库 小知识点&#xff1a; Linux中常见的软件安装方式 --------- 下载&&安装 a、yum/apt b、rpm安装包安装 c、源码安装 y…

SDK(1.1note)

什么是SDK 控制台程序&#xff08;Console User interface&#xff09;CUI 对于控制台程序&#xff0c;通用&#xff0c;也就是平台无关 图形界面系统 可是如果你想要播放一个音乐&#xff0c;C语言有嘛&#xff1f; 所以需要平台开发&#xff0c;不可以移植性 平台会提供…

摒弃“流量思维”,以精准流量驱动企业发展——基于开源 AI 智能名片、链动 2+1 模式及 O2O 商城小程序的思考

摘要&#xff1a;本文深入探讨在当前竞争激烈的营销环境下&#xff0c;摒弃“流量思维”的紧迫性与必要性。强调做内容营销不能仅仅局限于发文案&#xff0c;而应摆脱一味追求阅读量、推荐量和粉丝数的误区&#xff0c;聚焦于获取精准流量。结合开源 AI 智能名片、链动 21 模式…

如何解决npm下载Puppeteer卡死的问题

亲测有效 export PUPPETEER_DOWNLOAD_BASE_URLhttps://cdn.npmmirror.com/binaries/chrome-for-testingnpm install https://github.com/cnpm/binary-mirror-config/pull/47https://github.com/cnpm/binary-mirror-config/pull/47 PS: 最开始看了下面这两个链接&#xff0c…

网络安全-长亭雷池的sql绕过

一、环境 雷池官网docker安装我的版本是 看官网介绍主要防御top10 二、讲解 我这里只描述通用型绕过&#xff0c;对于事件型不多描述&#xff0c;因为通用型的绕过是通杀的&#xff0c;差异化绕过 正常来说我们是因为没有一个很好的过滤所以造成第11关靶场的绕过 但是现在有了…

Stable Diffusion进阶篇:模型训练(附秋叶模型训练器)

前言 今天就来学习下Stable Diffusion关于微调训练方面的知识点。 今天没多少废话&#xff0c;因为我下午要去玩PTCG&#xff01;让我们冲冲冲 整理和输出教程属实不易&#xff0c;觉得这篇教程对你有所帮助的话&#xff0c;可以点击&#x1f447;二维码领取资料&#x1f618…

k8s介绍-搭建k8s

官网&#xff1a;https://kubernetes.io/ 应用部署方式演变 传统部署&#xff1a;互联网早期&#xff0c;会直接将应用程序部署在物理机上 优点&#xff1a;简单&#xff0c;不需要其他技术的参与 缺点&#xff1a;不能为应用程序定义资源使用边界&#xff0c;很难合理地分配计…

Active Directory 实验室设置第二部分- 添加数据到AD域

在之前的文章中&#xff0c;我们已经讨论了AD森林的安装和AD基础知识。在这篇文章中&#xff0c;让我们开始使用 AD 对象&#xff08;如用户对象、计算机对象、组对象、网络共享等&#xff09;填充 AD 环境&#xff0c;以及计算机如何加入域。 #1、添加用户对象 可以使用GUI、…

什么是CPQ?一文讲解什么是CPQ选型配置报价系统

什么是CPQ选型配置报价? CPQ&#xff0c;是英文名 Configure Price Quote的缩写&#xff0c;中文称为配置报价软件。该软件经常出现在销售行业。它是一种可以快速为企业报价的销售工具。企业在报价时&#xff0c;会综合考虑数量、折扣、产品可选功能等。CPQ软件能够整合企业的…