Java 多线程(三)—— 死锁

ops/2024/10/15 22:10:01/

死锁的产生

我们先从简单的死锁最后到难一些的死锁问题开始展开讨论。

首先一个线程,一把锁,因为多次加锁而导致死锁问题,由于Java 的synchronized 实现了可重入锁,因此这个死锁问题就不存在了,意味着当一个线程拥有一把锁时,可以对这个锁进行多次加锁操作,而不会发生死锁问题,在上一篇文章中也仔细讨论了,这里不赘述了。

接下来就是两个线程,两把锁,两个线程都想获得对方的锁的时候,就会发生死锁问题,我们来看一下代码:

java">    public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("t1 线程获得了 锁1");synchronized (locker2) {System.out.println("t1 线程成功获得了 锁2");}}});Thread t2 = new Thread(() -> {synchronized(locker2) {System.out.println("t2 线程获得了 锁2");synchronized(locker1) {System.out.println("t2 线程成功获得了 锁1");}}});t1.start();t2.start();}

没想到,居然执行成功了!!!

在这里插入图片描述

为什么会执行成功呢?
因为t1.start() 的速度太快了,直接就获得了两把锁,t2 此时都还没开始执行就结束了。
但是这是一种大概率的情况,如果发生小概率事件,也就是死锁状态,程序就会一直卡住在这里

小概率事件是指什么?
理论上,当 t1 线程和 t2 线程开始执行的时候,t1 会获得 locker1, t2 获得 locker2 , 由于 t1 线程还需要获得 locker2 , t2 线程还需要获得 locker1,但是都被对方先拿到了,此时着两个线程就无法继续执行下去,就会导致两个线程一直处于阻塞状态

如何看到两个线程阻塞状态?
很简单,在 t1 线程加个 sleep, 保证 t2 线程获得了 locker2

java">    public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("t1 线程获得了 锁1");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("t1 线程成功获得了 锁2");}}});Thread t2 = new Thread(() -> {synchronized(locker2) {System.out.println("t2 线程获得了 锁2");synchronized(locker1) {System.out.println("t2 线程成功获得了 锁1");}}});t1.start();t2.start();}

在这里插入图片描述
我们打开 Jconsole

在这里插入图片描述

在这里插入图片描述

我们看到两个线程进入了 BLOCKED 状态(阻塞状态),并且还能直到代码阻塞在第几行。

这就是死锁问题


最后n 个线程,m 把锁,这个就要拿出经典案例:哲学家就餐问题

在这里插入图片描述

现在图中有 6 位哲学家,每位哲学家的左手和右手两边各有一根筷子,哲学家此时会有两个事件随机发生,一个事件是拿起左手和右手的筷子吃面,另一个事件就是思考哲学问题(不吃面)

现在我们试想一个极端的情况,如果每一个哲学家此时都想吃面,他们同时拿起左边的筷子,这时候,没有一位哲学家是拿到一双筷子的,并且没有一位哲学家会放弃自己左手的筷子,都在等别人放下的筷子之后拿起来吃面,这时候谁都吃不成。

上面就是经典的哲学家问题,上面的哲学家可以类比我们的线程,筷子就是锁,虽然这种死锁发生的概率很低,但是我们还是要防患于未然。

这种死锁是循环等待导致的,A 等待 B,B 等待 C,C 等待 A,构成一个回路。

死锁发生的原因

首先要回到锁的特性,因为锁是互斥的,一个线程拿到这个锁之后,另一个线程如果想要获得这个锁就必须阻塞等待。

锁是不可抢占的,不可剥夺的。 一个线程拿到这个锁之后,除非这个线程解锁释放这个锁,否则其他线程是无法暴力抢占获取的。

请求和保持。 这是发生在嵌套的情况下,也就是一个线程拿到 锁1 之后,在不释放锁 1 的前提下,申请获得锁 2 ,是有可能发生死锁的

循环等待。 多个线程,多把锁,在等待的过程中构成了循环,A 等待 B, B 也等待 A

解决死锁的办法

首先回顾第一个和第二个产生死锁的原因,我们直到这个是锁的基本性质引出的,所以我们无力回天,除非你自己写一个锁的设置。

破除嵌套

那我们来看一下第三个问题怎么解决,只要我们避免不要嵌套加锁就可以了,也就是用完锁 1 然后释放掉,最后再申请锁2 即可。

下面是死锁代码:

java">        Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("t1 线程获得了 锁1");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2) {System.out.println("t1 线程成功获得了 锁2");}}});Thread t2 = new Thread(() -> {synchronized(locker2) {System.out.println("t2 线程获得了 锁2");synchronized(locker1) {System.out.println("t2 线程成功获得了 锁1");}}});t1.start();t2.start();}

下面的破除嵌套之后的代码:

java">    public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(() -> {synchronized (locker1) {System.out.println("t1 线程获得了 锁1");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}synchronized (locker2) {System.out.println("t1 线程成功获得了 锁2");}});Thread t2 = new Thread(() -> {synchronized(locker2) {System.out.println("t2 线程获得了 锁2");}synchronized(locker1) {System.out.println("t2 线程成功获得了 锁1");}});t1.start();t2.start();}

运行成功,没有发生死锁问题。
在这里插入图片描述

破除循环等待

我们可以实现约定好加锁的顺序,就可以破除循环等待了。

例如,我们约定每个线程加锁的时候永远都是获得序号小的锁,然后获得序号大的锁。


http://www.ppmy.cn/ops/126132.html

相关文章

测试用例的编写

1.基本概念: 编写测试用例是确保代码质量和正确性的重要环节,尤其是在软件开发和维护过程中。测试用例通常用于验证功能是否符合预期,并及时发现潜在的错误或漏洞。 2.常见的测试用例编写方法: 等价划分法,边界值法&a…

线性回归损失函数的推导

要推导损失函数公式 ℓ ( θ ) 1 2 n ( y ^ − y ) ⊤ ( y ^ − y ) \ell(\boldsymbol{\theta}) \frac{1}{2n}(\hat{\boldsymbol{y}} - \boldsymbol{y})^\top(\hat{\boldsymbol{y}} - \boldsymbol{y}) ℓ(θ)2n1​(y^​−y)⊤(y^​−y),我们可以从几个基础概念开…

安装R和RStudio:开始你的数据分析之旅

数据分析是当今世界中一个非常热门的领域,而R语言是进行数据分析的强大工具之一。R是一种编程语言和软件环境,用于统计计算和图形表示。RStudio是一个集成开发环境(IDE),它为R语言提供了一个更加友好和高效的工作环境。…

Python | Leetcode Python题解之第474题一和零

题目: 题解: class Solution:def findMaxForm(self, strs: List[str], m: int, n: int) -> int:count10 []for s in strs:count10.append([0,0])for c in s:if c 0: count10[-1][0]1else: count10[-1][1]1dp [[0]*(n1) for _ in range(m1)]for i …

Go 语言中的格式化占位符

在 Go 语言中,fmt 包提供了大量的格式化占位符,用于格式化输出不同类型的数据。选择合适的占位符,可以确保输出的内容格式正确、清晰易懂。 常见的占位符: 基本类型 %v:按值的默认格式输出。适用于任何类型。%v&…

vue3.0 + vite:中使用 sass

1、安装依赖 npm i sass sass-loader --save-dev 在项目的src/assets文件夹下新建style/index.scss 文件 2、在 vite.config.ts 中加: resolve: {alias: {: fileURLToPath(new URL(./src, import.meta.url))} }, css: {// 配置 SCSS 支持preprocessorOptions: {s…

Codeforces Round 946 (Div. 3) F题 Cutting Game(双指针,模拟)

题目链接 Codeforces Round 946 (Div. 3) F题 Cutting Game 思路 维护两个数组,一个按照 x x x坐标排序,一个按照 y y y坐标排序。 使用双指针,维护两个数组剩下的点的头和尾。 最后直接模拟即可。 代码 #pragma GCC optimize("O…

全流程信息收集方法总结

信息收集 信息收集是指通过各种方式获取所需要的信息,以便我们在后续的渗透过程更好的进行。最简单的比如说目标站点的IP、中间件、脚本语言、端口、邮箱等等。我觉得信息收集在我们渗透测试的过程当中,是最重要的一环,这一环节没做好&#…