Java 并发编程:如何防止在线程阻塞与唤醒时死锁

简介: 首发公众号:码农架构

image.png

多线程如何实现阻塞与唤醒 说到suspend与resume组合有死锁倾向,一不小心将导致很多问题,甚至导致整个系统崩溃。接着看另外一种解决方案,我们可以使用以对象为目标的阻塞,即利用Object类的wait()和notify()方法实现线程阻塞。当线程到达监控对象时,通过wait方法会使线程进入到等待队列中。而当其它线程调用notify时则可以使线程重新回到执行队列中,得以继续执行

思维不同

针对对象的阻塞编程思维需要我们稍微转变下思维,它与面向线程阻塞思维有较大差异。如前面的suspend与resume只需在线程内直接调用就能完成挂起恢复操作,这个很好理解。而如果改用wait与notify形式则是通过一个object作为信号,可以将其看成是一堵门。object的wait()方法是锁门的动作,notify()是开门的动作。某一线程一旦关上门后其他线程都将阻塞,直到别的线程打开门。
image.png

如图所示,一个对象object调用wait()方法则像是堵了一扇门。线程一、线程二都将阻塞,然后线程三调用object的notify()方法打开门,准确地说是调用了notifyAll()方法,notify()仅仅能让线程一或线程二其中一条线程通过)。最终线程一、线程二得以通过。



## 死锁问题解决了吗?
使用wait与notify能在一定程度上避免死锁问题,但并不能完全避免,它要求我们必须在编程过程中避免死锁。在使用过程中需要注意的几点是:

  • 首先,wait与notify方法是针对对象的,调用任意对象的wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁。相应地,调用任意对象的notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行。
  • 其次,wait与notify方法必须在synchronized块或方法中被调用,并且要保证同步块或方法的锁对象与调用wait与notify方法的对象是同一个。如此一来在调用wait之前当前线程就已经成功获取某对象的锁,执行wait阻塞后当前线程就将之前获取的对象锁释放。当然假如你不按照上面规定约束编写,程序一样能通过编译,但运行时将抛出IllegalMonitorStateException异常,必须在编写时保证用法正确。
  • 最后,notify是随机唤醒一条阻塞中的线程并让之获取对象锁,进而往下执行,而notifyAll则是唤醒阻塞中的所有线程,让他们去竞争该对象锁,获取到锁的那条线程才能往下执行。

改进例子

我们通过wait与notify改造前面的例子,代码如下。改造的思想就是在MyThread中添加一个标识变量,一旦变量改变就相应地调用wait和notify阻塞唤醒线程。由于在执行wait后将释放synchronized(this)锁住的对象锁,此时System.out.println("running….");早已执行完毕,System类out对象不存在死锁问题。
image.png
image.png

Park与UnPark

wait与notify组合的方式看起来是个不错的解决方式,但其面向的主体是对象object,阻塞的是当前线程,而唤醒的是随机的某个线程或所有线程,偏重于线程之间的通信交互。假如换个角度,面向的主体是线程的话,我就能轻而易举地对指定的线程进行阻塞唤醒,这个时候就需要LockSupport,它提供的park与unpark方法分别用于阻塞和唤醒.而且它提供避免死锁和竞态条件,很好地代替suspend和resume组合。
image.png
用park与unpark改造上述例子,代码如下。把主体换成线程进行的阻塞看起来貌似比较顺眼,而且由于park与unpark方法控制的颗粒度更加细小,能准确决定线程在某个点停止,进而避免死锁的产生。例如此例中在执行System.out.println前线程就被阻塞了,于是不存在因竞争System类out对象而产生死锁,即便在执行System.out.println后线程才阻塞也不存在死锁问题,因为锁已释放。
image.png

LockSupport 优势

LockSupport类为线程阻塞唤醒提供了基础,同时,在竞争条件问题上具有wait和notify无可比拟的优势。使用wait和notify组合时,某一线程在被另一线程notify之前必须要保证此线程已经执行到wait等待点,错过notify则可能永远都在等待,另外notify也不能保证唤醒指定的某线程。反观LockSupport,由于park与unpark引入了许可机制,许可逻辑为:

  • park将许可在等于0的时候阻塞,等于1的时候返回并将许可减为0。
  • unpark尝试唤醒线程,许可加1。

根据这两个逻辑,对于同一条线程,park与unpark先后操作的顺序似乎并不影响程序正确地执行。假如先执行unpark操作,许可则为1,之后再执行park操作,此时因为许可等于1直接返回往下执行,并不执行阻塞操作。 最后,LockSupport的park与unpark组合真正解耦了线程之间的同步,不再需要另外的对象变量存储状态,并且也不需要考虑同步锁,wait与notify要保证必须有锁才能执行,而且执行notify操作释放锁后还要将当前线程扔进该对象锁的等待队列,LockSupport则完全不用考虑对象、锁、等待队列等问题。

相关文章
|
5天前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
44 17
|
16天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
1天前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
18天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
15天前
|
安全 Java API
【JavaEE】多线程编程引入——认识Thread类
Thread类,Thread中的run方法,在编程中怎么调度多线程
|
7月前
|
Java C++
关于《Java并发编程之线程池十八问》的补充内容
【6月更文挑战第6天】关于《Java并发编程之线程池十八问》的补充内容
54 5
|
4月前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。
|
6月前
|
安全 Java 开发者
Java中的并发编程:深入理解线程池
在Java的并发编程中,线程池是管理资源和任务执行的核心。本文将揭示线程池的内部机制,探讨如何高效利用这一工具来优化程序的性能与响应速度。通过具体案例分析,我们将学习如何根据不同的应用场景选择合适的线程池类型及其参数配置,以及如何避免常见的并发陷阱。
63 1
|
6月前
|
监控 Java
Java并发编程:深入理解线程池
在Java并发编程领域,线程池是提升应用性能和资源管理效率的关键工具。本文将深入探讨线程池的工作原理、核心参数配置以及使用场景,通过具体案例展示如何有效利用线程池优化多线程应用的性能。
|
5月前
|
Java 数据库
Java中的并发编程:深入理解线程池
在Java的并发编程领域,线程池是提升性能和资源管理的关键工具。本文将通过具体实例和数据,探讨线程池的内部机制、优势以及如何在实际应用中有效利用线程池,同时提出一个开放性问题,引发读者对于未来线程池优化方向的思考。
49 0