我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java多线程之死锁问题,wait和notify

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Java多线程之死锁问题,wait和notify

文章目录


这个博客研究的死锁问题是基于Java当中进行叙述的, 而在Java当中, 与死锁问题息息相关的就是 synchronized关键字了.

一. synchronnized 的特性

1. 互斥性

synchronized 会起到互斥效果, 这里的互斥其实很好理解, 一个线程执行到某个对象的 synchronized 中时, 此时就是针对这个对象加锁了, 而如果此时其他线程如果也想要使用 synchronized 针对同一个对象进行加锁, 就必须等到该对象对象上的锁释放掉才行, 这便是互斥的效果了.

2. 可重入性

同一个线程针对同一个对象, 连续加锁两次, 是否会有问题; 如果没问题, 就是可重入的, 如果有问题, 就是不可重入的.

看下面的代码, 在Java当中是可行的.

class Counter {    public int count = 0;    synchronized public void add() {        synchronized (this) {            count++;        }    }}

这里的锁对象是this只要有线程调用add, 进入add方法的时候,就会先加锁(能够加锁成功), 紧接着又遇到了代码块, 再次尝试加锁.

站在this的视角(锁对象)它认为自己已经被另外的线程给占用了, 这里的第二次加锁是否要阻塞等待呢? 如果这里的第二次获取锁成功, 这个锁就是可重入的, 如果进入阻塞等待的状态, 就是不可重入的, 此时如果进入了阻塞等待大的状态, 可想而知, 我们的程序就 “僵住了” , 这也就是是一种死锁的情况了.

上面的代码在Java代码中是很容易出现的, 为了避免上面所说情况的出现, Java中 synchronized 就被设置成可重入的了.

synchronized可重入的特性其实就是是在锁对象里面记录一下, 当前的锁是哪个线程持有的, 如果再次加锁的线程和持有线程是同一个, 就可以获取锁, 否则就阻塞等待.

二. 死锁问题

1. 什么是死锁

死锁是指两个或两个以上的进程在执行过程中, 由于竞争资源或者由于彼此通信而造成的一种阻塞的现象, 若无外力作用, 它们都将无法推进下去; 此时称系统处于死锁状态或系统产生了死锁, 这些永远在互相等待的进程称为死锁进程; 通俗点说, 死锁就是两个或者多个相互竞争资源的线程, 你等我, 我等你, 你不放我也不放, 这就造成了他们之间的互相等待, 导致了 “永久” 阻塞.

一旦程序出现死锁, 就会导致线程无法继续执行后续的工作, 程序势必会有严重的bug, 而且是死锁非常隐蔽的, 开发阶段, 不经意间, 就会写出死锁代码, 还不容易测试出来, 所以这就需要我们对死锁问题有一定的认识以方便我们以后的调试和修改.

2. 死锁的四个必要条件

  1. 互斥使用: 线程1拿到了锁, 线程2就得进入阻塞状态(锁的基本特性).
  2. 不可抢占: 线程1拿到锁之后, 必须是线程1主动释放, 不可能线程1还没有释放, 线程2强行获取到锁.
  3. 请求和保持: 线程1拿到锁A后, 再去获取锁B的时候, A这把锁仍然保持, 不会因为要获取锁B就把A释放了.
  4. 循环等待: 线程1先获取锁A再获取锁B, 线程2先获取锁B再获取锁A, 线程1在获取锁B的时候等待线程2释放B,同时线程2在获取锁A的时候等待线程1释放A.

而在Java代码中, 前三点 synchronized锁的基本特性, 我们是无法改变的, 循环等待是这四个条件里唯一 一个和代码结构相关的, 是我们可以控制的.

3. 常见的死锁场景及解决

3.1 不可重入造成的死锁

同一个线程针对同一个对象, 连续加锁两次, 如果锁不是可重入锁, 就会造成死锁问题.

最开始介绍synchronized的特性的时候所说, synchronized具有可重入性, 而在Java中还有一个ReentrantLock锁也是可重入锁, 所以说, 在Java程序中, 不会出现这种死锁问题.

3.2 循环等待的场景

哲学家就餐问题(多个线程多把锁)

  • 场景

img

有五位沉默的哲学家围坐在一张圆桌旁, 每个哲学家有两种状态.

  1. 思考人生(相当于线程的阻塞状态)
  2. 拿起筷子吃面条(相当于线程获取到锁然后执行一些计算)

有五只筷子供他们使用, 哲学家需要拿到左手和右手边的两根筷子之后才能吃饭, 吃完后将筷子放下继续思考.

由于操作系统随机调度, 这五个哲学家, 随时都可能想吃面条, 也随时可能要思考人生.

假设出现了极端情况, 同─时刻, 所有的哲学家同时拿起右手的筷子, 哲学家们需要再拿起左手的筷子才可以吃面条, 而此时他们发现没有筷子可以拿了, 都在等左边的哲学家放下筷子, 这里的筷子落实到程序中就相当于锁, 此时就陷入了互相阻塞等待的状态, 这种场景就是典型的因为循环等待造成的死锁问题.

img

  • 解决方案

我们可以给按筷子编号, 哲学家们拿筷子时需要遵守一个规则, 拿筷子需要先拿编号小的, 再拿编号大的, 再来看这个场景, 哲学家 2, 3, 4, 5 分别拿起了两手边编号为 1, 2, 3, 4 编号较小的筷子, 而1号哲学家想要拿到编号编号较小的1号筷子发现已经被拿走了, 此时就空出了5号筷子, 这样5号哲学家就可以拿起5号筷子去吃面条了, 等5号哲学家放下筷子后, 4号哲学家就可以拿起4号筷子去吃面条了, 以此类推…

img

对应到程序中, 这样的做法其实就是在给锁编号, 然后再按照一个规定好的顺序来加锁, 任意线程加多把锁的时候, 都让线程遵守这个顺序, 这样就解决了互相阻塞等待的问题.

两个线程两把锁

两个线程两把锁, t1, t2线程先各自针对锁A, 锁B加锁, 然后再去获取对方的锁, 此时双方就会陷入僵持状态, 造成了死锁问题.

img

这里可以看一下这里举出来的现实中的例子来理解这里的场景:

前段时间疫情还没有放开的时候, 走到哪里都离不开健康码, 某一天这个健康码就给给崩了, 手机上的健康码没办法正常打开了, 于是程序员就赶到公司去修复这个bug, 但是在公司楼下被保安拦住了, 保安要求出示健康码才能上楼, 程序员说: “健康码出问题了, 我上楼修复了才能出示健康码” ; 保安又说: “你出示了健康码才能上楼”; 此时场景就陷入了僵持的状态, 程序员上不了楼, 健康码也无法修复; 这个场景就可以类比这里的锁问题.

观察下面的代码及执行结果:

这里的代码是为了构造一个死锁的场景, 代码中的sleep是为了确保两个线程先把第一个锁拿到, 因为线程是抢占式执行的, 如果没有sleep的作用, 这里的死锁场景是不容易构造出来的.

public class TestDemo14 {    public static void main(String[] args) {        Object A = new Object();        Object B = new Object();        Thread t1 = new Thread(() -> {            synchronized (A) {                System.out.println(Thread.currentThread().getName()+"获取到了锁A");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println(Thread.currentThread().getName()+"获取到了锁B");                }            }        }, "t1");        Thread t2 = new Thread(() -> {            synchronized (B) {                System.out.println(Thread.currentThread().getName()+"获取到了锁B");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (A) {                    System.out.println(Thread.currentThread().getName()+"获取到了锁A");                }            }        }, "t2");        t1.start();        t2.start();    }}

执行结果:

img

看这里的执行结果, t1线程获取到了锁A但并没有获取到锁B, t2线程获取到了锁B但并没有获取到锁A, 也就是说t1t2两个线程进入了相互阻塞的状态, 线程无法获去到两把锁, 我们可以使用jconsole工具来观察一下这两个线程的状态, 分析一下是哪里的代码造成这里死锁问题的.

可以发现, t1线程此时是处于BLOCKED状态的, 表示获取锁, 获取不到的阻塞状态; 根据堆栈跟踪的信息反映在代码中是在第14行.

img

同样的, t2线程此时也是处于BLOCKED阻塞状态的; 根据堆栈跟踪的信息反映在代码中是在第27行.

img

上面叙述的是两个线程死锁问题的代码场景和具体分析, 那么这里的锁问题如何解决呢?

其实也不需要特别复杂的算法, 实际开发中只需要解单高效的解决问题即可, 复杂了反而会使程序容易出bug, 可能会引出新的问题, 就比如上面介绍的哲学家就餐问题通过限制加锁顺序来解决死锁问题就是一种简单高效的解决办法, 而这里也一样, 也可以通过控制加锁的顺序来解决, 我们让t1t2两个线程都按照相同的顺序来获取锁, 比如这里规定先获取锁A, 再获取锁B, 这样按照相同的顺序去获取锁就避免了循环等待造成的死锁问题, 代码如下:

public class TestDemo14 {    public static void main(String[] args) {        Object A = new Object();        Object B = new Object();        Thread t1 = new Thread(() -> {            synchronized (A) {                System.out.println(Thread.currentThread().getName()+"获取到了锁A");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println(Thread.currentThread().getName()+"获取到了锁B");                }            }        }, "t1");        Thread t2 = new Thread(() -> {            synchronized (A) {                System.out.println(Thread.currentThread().getName()+"获取到了锁B");                try {                    Thread.sleep(1000);                } catch (InterruptedException e) {                    throw new RuntimeException(e);                }                synchronized (B) {                    System.out.println(Thread.currentThread().getName()+"获取到了锁A");                }            }        }, "t2");        t1.start();        t2.start();    }}

最后的执行结果两个线程都获取到了A,B锁.

img

三. Object类中提供线程等待的方法

1. 常用方法

除了Thread类中的能够实现线程等待的方法, 如join, sleep, 在Object类中也提供了相关线程等待的方法.

方法解释
public final void wait() throws InterruptedException释放锁并使线程进入WAITING状态
public final native void wait(long timeout) throws InterruptedException相比于上面, 多了一个最长等待时间
public final void wait(long timeout, int nanos) throws InterruptedException等待的最长时间精度更大
public final native void notify();随机唤醒一个WAITING状态的线程, 并加锁, 搭配wait方法使用
public final native void notifyAll();唤醒所有处于WAITING状态的线程, 并加锁(很可能产生锁竞争), 搭配wait方法使用

我们知道由于线程之间的抢占式执行和操作系统的随机调度会导致线程之间执行顺序是 “随机” 的, 但在实际开发中很多场景下我们是希望可以协调多个线程之间的执行先后顺序的.

虽然线程在内核里的调度是随机的, 这个我们是没办法改变的, 但是我们可以通过一些api让线程主动阻塞, 主动放弃CPU来给别的线程让路, 以此来控制线程之间的执行顺序.

Thread类中的joinsleep方法定程度上也能控制线程的执行顺序, 但通过join和sleep控制并不够灵活:

  1. 使用join, 则必须要t1彻底执行完, t2才能执行; 如果是希望t1先干50%的活, 就让t2开始行动, join就无能为力了.
  2. 使用sleep, 指定一个休眠时间的, 但是t1执行的这些任务, 到底花了多少时间, 是不好估计的.

而使用waitnotify可以更好的解决上述的问题.

下面的代码t线程中没有使用synchronized进行加锁, 直接调用了wait方法, 会产生非法锁状态异常.

public class TestDemo15 {    public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(() -> {            try {                Thread.sleep(5000);            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println("执行完毕!");        });        t.start();        System.out.println("wait前");        t.wait();        System.out.println("wait后");    }}

执行结果:
img

之所以这里会抛出这个异常, 是因为wait方法的执行步骤为:

  1. 先释放锁
  2. 再让线程阻塞等待
  3. 最后满足条件后, 重新尝试获取锁, 并在获取到锁后, 继续往下执行

而上面的代码都没有加锁, 又怎么能释放锁锁呢, 所以会抛出异常, 所以说, wait操作需要搭配synchronized来使用.

所以对上面的代码做出如下修改即可,

synchronized (t) {    System.out.println("wait前");    t.wait();    System.out.println("wait后");}

执行结果:

img

2. wait和notify的搭配使用

wait方法常常搭配notify方法搭配一起使用, notify方法用来唤醒wait等待的线程, wait能够释放锁, 使线程等待, 而notify唤醒线程后能够获取锁, 然后使线程继续执行, 执行流程如下:

img

在Java中, notify方法也需要在加锁前提下使用.

代码示例:

public class TestDemo16 {    public static void main(String[] args) throws InterruptedException {        Object object = new Object();        Thread t1 = new Thread(() -> {            // 这个线程负责进行等待            System.out.println("t1: wait 之前");            try {                synchronized (object) {                    object.wait();                }            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println("t1: wait 之后");        });        Thread t2 = new Thread(() -> {            System.out.println("t2: notify 之前");            synchronized (object) {                // notify 务必要获取到锁, 才能进行通知                try {                    Thread.sleep(3000);                } catch (InterruptedException e) {                    e.printStackTrace();                }                object.notify();            }            System.out.println("t2: notify 之后");        });        t1.start();        // 此处写的 sleep 500 是大概率会让当前的 t1 先执行 wait 的.        // 极端情况下 (电脑特别卡的时候), 可能线程的调度时间就超过了 500 ms        // 还是可能 t2 先执行 notify.        Thread.sleep(500);        t2.start();    }}

执行结果:

img

注意事项:

  1. 虽然这里wait是阻塞了, 阻塞在synchronized代码块里, 实际上, 这里的阻塞是释放了锁的, 此时其他线程是可以获取到object这个对象的锁的, 这里的阻塞,就处在WAITING状态.

img

  1. 代码中的锁对象和调用wait, notify方法的对象必须是相同的才能够起到应有的效果, notify只能唤醒在同一个对象上等待的线程.

img

  1. 代码中要保证先执行wait, 后执行notify才是有意义的.

img

  1. wait无参数版本, 是一个死等的版本, 只要不进行notify, 就会死等下去, 可以采用wait带参数版本设计代码避免死等可能出现的问题.

3. wait 和 sleep 的区别

  • 相同点
  1. 都可以使线程暂停一段时间来控制线程之间的执行顺序.
  2. wait可以设置一个最长等待时间, 和sleep一样都可以提前唤醒.
  • 不同点
  1. wait是Object类中的一个方法, sleep是Thread类中的一个方法.
  2. wait必须在synchronized修饰的代码块或方法中使用, sleep方法可以在任何位置使用.
  3. wait被调用后当前线程进入BLOCK状态并释放锁,并可以通过notify和notifyAll方法进行唤醒;sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关的操作.
  4. 使用sleep只能指定一个固定的休眠时间, 线程中执行操作的执行时间是无法确定的; 而使用wait在指定操作位置就可以唤醒线程.
  5. sleep和wait都可以被提前唤醒, interruppt唤醒sleep, 是会报异常的, 这种方式是一个非正常的执行逻辑; 而noitify唤醒wait是正常的业务执行逻辑, 不会有任何异常.

4. 练习: 顺序打印ABC

有三个线程, 分别只能打印A, B, C, 实现代码控制三个线程固定按照ABC的顺序打印.

public class TestdDemo17 {    public static void main(String[] args) throws InterruptedException {        Object locker1 = new Object();        Object locker2 = new Object();        Thread t1 = new Thread(() -> {            System.out.println("A");            synchronized (locker1) {                locker1.notify();            }        });                Thread t2 = new Thread(() -> {            synchronized (locker1) {                try {                    locker1.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }            System.out.println("B");            synchronized (locker2) {                locker2.notify();            }        });                Thread t3 = new Thread(() -> {            synchronized (locker2) {                try {                    locker2.wait();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }            System.out.println("C");        });        t2.start();        t3.start();        Thread.sleep(100);        t1.start();    }}

执行结果:

img

来源地址:https://blog.csdn.net/Trong_/article/details/128541156

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java多线程之死锁问题,wait和notify

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java多线程死锁问题详解(wait和notify)

线程之间形成相互等待资源的环时,就会形成顺序死锁,下面这篇文章主要给大家介绍了关于Java多线程死锁问题(wait和notify)的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
2023-01-05

java多线程wait()和notify()如何使用

小编给大家分享一下java多线程wait()和notify()如何使用,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!注:wait()和notify()应当用在synchronized内package com.test;im
2023-06-02

Java多线程wait()和notify()方法详细图解

wait()和notify()是直接隶属于Object类,也就是说所有对象都拥有这一对方法,下面这篇文章主要给大家介绍了关于Java多线程wait()和notify()方法详细图解的相关资料,需要的朋友可以参考下
2022-11-13

Java多线程死锁问题怎么解决

解决Java多线程死锁问题的常用方法有以下几种:1. 避免使用多个锁:尽量减少使用多个锁来降低出现死锁的概率。2. 按照固定的顺序获取锁:对于多个锁的获取,确保线程按照固定的顺序获取锁,避免出现循环等待的情况。3. 设置超时时间:在获取锁的
2023-09-22

Java多线程之死锁的示例分析

小编给大家分享一下Java多线程之死锁的示例分析,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!什么是死锁?死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者
2023-05-30

Java 线程死锁的问题解决办法

Java 线程死锁的问题解决办法【线程死锁】 原因:两个线程相互等待被对方锁定的资源 代码模拟:public class DeadLock { public static void main(String[] args) { Ob
2023-05-31

Java线程技术中的死锁问题怎么解决

这篇文章主要介绍“Java线程技术中的死锁问题怎么解决”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Java线程技术中的死锁问题怎么解决”文章能帮助大家解决问题。我们知道,使用 synchroniz
2023-06-02

java多线程学习之死锁的模拟和避免(实例讲解)

1.死锁死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。Java 死锁产生的四个必要条件:1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用2
2023-05-31

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录