【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案

简介:

【Java并发基础】使用“等待—通知”机制优化死锁中占用且等待解决方案

阅读目录
前言
就医流程—完整的“等待—通知”机制
Java中“等待—通知”机制的实现
如何使线程等待,wait()
如何唤醒线程,notify()/notifyAll()
使用“等待-通知”机制重写转账
一些需要注意的问题
sleep()和wait()的区别
为什么wait()、notify()、notifyAll()是定义在Object中,而不是Thread中?
小结

回到目录
前言
在前篇介绍死锁的文章中,我们破坏等待占用且等待条件时,用了一个死循环来获取两个账本对象。

java
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
  ;
我们提到过,如果apply()操作耗时非常短,且并发冲突量也不大,这种方案还是可以。否则的话,就可能要循环上万次才可以获取锁,这样的话就太消耗CPU了!

于是我们给出另一个更好的解决方案,等待-通知机制:
若是线程要求的条件不满足,则线程阻塞自己,进入等待状态;当线程要求的条件满足时,通知等待的线程重新执行。

Java是支持这种等待-通知机制的,下面我们就来详细介绍这个机制,并用这个机制来优化我们的转账流程。
我们先通过一个就医流程来了解一个完善的“等待-通知”机制。

回到目录
就医流程—完整的“等待—通知”机制
在医院就医的流程基本是如下这样:

患者先去挂号,然后到就诊门口分诊,等待叫号;
当叫到自己的号时,患者就可以找医生就诊;
就诊过程中,医生可能会让患者去做检查,同时叫一位患者;
当患者做完检查后,拿着检查单重新分诊,等待叫号;
当医生再次叫到自己时,患者就再去找医生就诊。
我们将上述过程对应到线程的运行情况:

患者到就诊门口分诊,类似于线程要去获取互斥锁;
当患者被叫到号时,类似于线程获取到了锁;
医生让患者去做检查(缺乏检查报告不能诊断病因),类似于线程要求的条件没有满足;
患者去做检查,类似于线程进入了等待状态;然后医生叫下一个患者,意味着线程释放了持有的互斥锁;
患者做完检查,类似于线程要求的条件已经满足;患者拿着检查报告重新分诊,类似于线程需要重新获取互斥锁。
一个完整的“等待—通知”机制如下:
线程首先获取互斥锁,当线程要求条件不满足时,释放互斥锁,进入等待状态;当条件满足时,通知等待的线程,重新获取锁。

一定要理解每一个关键点,还需要注意,通知的时候虽然条件满足了,但是不代表该线程再次获取到锁时,条件还是满足的。

回到目录
Java中“等待—通知”机制的实现
在Java中,等待—通知机制可以有多种实现,这里我们讲解由synchronized配合wait()、notify()或者notifyAll()的实现。

如何使线程等待,wait()
当线程进入获取锁进入同步代码块后,若是条件不满足,我们便调用wait()方法使得当前线程被阻塞且释放锁。

上图中的等待队列和互斥锁是一一对应的,每个互斥锁都有自己的独立的等待队列(等待队列是同一个)。(这句话还在暗示我们后面唤醒线程时,是唤醒对应锁上的线程。)

如何唤醒线程,notify()/notifyAll()
当条件满足时,我们调用notify()或者notifyAll(),通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过。

我们要在相应的锁上使用wait() 、notify()和notifyAll()。
需要注意,这三个方法可以被调用的前提是我们已经获取到了相应的互斥锁。所以,我们会发现wait() 、notify() notifyAll()都是在synchronized{...}内部中被调用的。如果在synchronized外部调用,JVM会抛出异常:java.lang.IllegalMonitorStateException。

回到目录
使用“等待-通知”机制重写转账
我们现在使用“等待—通知”机制来优化上篇的一直循环获取锁的方案。首先我们要清楚如下如下四点:

互斥锁:账本管理员Allocator是单例,所以我们可以使用this作为互斥锁;
线程要求的条件:转出账户和转入账户都存在,没有被分配出去;
何时等待:线程要求的条件不满足则等待;
何时通知:当有线程归还账户时就通知;
使用“等待—通知”机制时,我们一般会套用一个“范式”,可以看作是前人的经验总结用法。

java
while(条件不满足) {

wait();

}
这个范式可以解决“条件曾将满足过”这个问题。因为当wait()返回时,条件已经发生变化,使用这种结构就可以检验条件是否还满足。

解决我们的转账问题:

java
class Allocator {

private List<Object> als;
// 一次性申请所有资源
synchronized void apply(Object from, Object to){
    // 经典写法
    while(als.contains(from) || als.contains(to)){ 
        // from 或者 to账户被其他线程拥有
        try{
            wait(); // 条件不满足时阻塞当前线程
        }catch(Exception e){
        }   
    }
    als.add(from);
    als.add(to);  
}
// 归还资源
synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();   // 归还资源,唤醒其他所有线程
}

}
回到目录
一些需要注意的问题

sleep()和wait()的区别
sleep()和wait()都可以使线程阻塞,但是它们还是有很大的区别:

wait()方法会使当前线程释放锁,而sleep()方法则不会。
当调用wait()方法后,当前线程会暂停执行,并进入互斥锁的等待队列中,直到有线程调用了notify()或者notifyAll(),等待队列中的线程才会被唤醒,重新竞争锁。
sleep()方法的调用需要指定等待的时间,它让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,但是它不会使线程释放锁,这意味其他线程在当前线程阻塞的时候,是不能进入获取锁,执行同步代码的。
wait()只能在同步方法或者同步代码块中执行,而sleep()可以在任何地方执行。
使用wait()无需捕获异常,而使用sleep()则必须捕获。
wait()是Object类的方法,而sleep是Thread的方法。

为什么wait()、notify()、notifyAll()是定义在Object中,而不是Thread中?
wait()、notify()以及notifyAll()它们之间的联系是依靠互斥锁,也就同步锁(内置锁),我们前面介绍过,每个Java对象都可以用作一个实现同步的锁,所以这些方法是定义在Object中,而不是Thread中。

回到目录
小结
“等待—通知”机制是一种非常普遍的线程间协作的方式,我们在理解时可以利用生活中的例子去类似,就如上面的就医流程。上文中没有明显说明notify()和notifyAll()的区别,只是在图中标注了一下。我们建议尽量使用notifyAll(),notify() 是会随机地通知等待队列中的一个线程,在极端情况下可能会使某个线程一直处于阻塞状态不能去竞争获取锁导致线程“饥饿”;而 notifyAll() 会通知等待队列中的所有线程,即所有等待的线程都有机会去获取锁的使用权。

参考:
[1]极客时间专栏王宝令《Java并发编程实战》
[2]Brian Goetz.Tim Peierls. et al.Java并发编程实战[M].北京:机械工业出版社,2016
[3]skywang12345.Java多线程系列--“基础篇”05之 线程等待与唤醒.https://www.cnblogs.com/skywang12345/p/3479224.html

原文地址https://www.cnblogs.com/myworld7/p/12231936.html

相关文章
|
14天前
|
Java
Java 字符串分割split空字符串丢失解决方案
Java 字符串分割split空字符串丢失解决方案
|
18天前
|
存储 缓存 算法
优化 Java 后台代码的关键要点
【4月更文挑战第5天】本文探讨了优化 Java 后台代码的关键点,包括选用合适的数据结构与算法、减少不必要的对象创建、利用 Java 8 新特性、并发与多线程处理、数据库和缓存优化、代码分析与性能调优、避免阻塞调用、JVM 调优以及精简第三方库。通过这些方法,开发者可以提高系统性能、降低资源消耗,提升用户体验并减少运营成本。
|
4天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
6天前
|
Java 开发者
Java中多线程并发控制的实现与优化
【4月更文挑战第17天】 在现代软件开发中,多线程编程已成为提升应用性能和响应能力的关键手段。特别是在Java语言中,由于其平台无关性和强大的运行时环境,多线程技术的应用尤为广泛。本文将深入探讨Java多线程的并发控制机制,包括基本的同步方法、死锁问题以及高级并发工具如java.util.concurrent包的使用。通过分析多线程环境下的竞态条件、资源争夺和线程协调问题,我们提出了一系列实现和优化策略,旨在帮助开发者构建更加健壮、高效的多线程应用。
7 0
|
7天前
|
存储 缓存 安全
Java并发基础之互斥同步、非阻塞同步、指令重排与volatile
在Java中,多线程编程常常涉及到共享数据的访问,这时候就需要考虑线程安全问题。Java提供了多种机制来实现线程安全,其中包括互斥同步(Mutex Synchronization)、非阻塞同步(Non-blocking Synchronization)、以及volatile关键字等。 互斥同步(Mutex Synchronization) 互斥同步是一种基本的同步手段,它要求在任何时刻,只有一个线程可以执行某个方法或某个代码块,其他线程必须等待。Java中的synchronized关键字就是实现互斥同步的常用手段。当一个线程进入一个synchronized方法或代码块时,它需要先获得锁,如果
23 0
|
7天前
|
SQL 缓存 Java
Java数据库连接池:优化数据库访问性能
【4月更文挑战第16天】本文探讨了Java数据库连接池的重要性和优势,它能减少延迟、提高效率并增强系统的可伸缩性和稳定性。通过选择如Apache DBCP、C3P0或HikariCP等连接池技术,并进行正确配置和集成,开发者可以优化数据库访问性能。此外,批处理、缓存、索引优化和SQL调整也是提升性能的有效手段。掌握数据库连接池的使用是优化Java企业级应用的关键。
|
9天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
10天前
|
Java 编译器
Java并发编程中的锁优化策略
【4月更文挑战第13天】 在Java并发编程中,锁是一种常见的同步机制,用于保证多个线程之间的数据一致性。然而,不当的锁使用可能导致性能下降,甚至死锁。本文将探讨Java并发编程中的锁优化策略,包括锁粗化、锁消除、锁降级等方法,以提高程序的执行效率。
13 4
|
15天前
|
存储 缓存 安全
【企业级理解】高效并发之Java内存模型
【企业级理解】高效并发之Java内存模型
|
16天前
|
设计模式 缓存 安全
分析设计模式对Java应用性能的影响,并提供优化策略
【4月更文挑战第7天】本文分析了7种常见设计模式对Java应用性能的影响及优化策略:单例模式可采用双重检查锁定、枚举实现或对象池优化;工厂方法和抽象工厂模式可通过对象池和缓存减少对象创建开销;建造者模式应减少构建步骤,简化复杂对象;原型模式优化克隆方法或使用序列化提高复制效率;适配器模式尽量减少使用,或合并多个适配器;观察者模式限制观察者数量并使用异步通知。设计模式需根据应用场景谨慎选用,兼顾代码质量和性能。

热门文章

最新文章