首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
当前位置:
首页>>
技术小册>>
Java并发编程实战
小册名称:Java并发编程实战
### 06 | 用“等待-通知”机制优化循环等待 在Java并发编程中,处理线程间的同步与协作是至关重要的一环。传统的循环等待(也称为忙等待或自旋等待)方式,虽然简单直观,但在多线程环境中却极易导致CPU资源的浪费和效率低下。特别是当线程需要等待某个条件成立时,如果采用循环检查该条件的方式,不仅会无谓地消耗CPU资源,还可能因为过高的CPU占用率而影响到系统的整体性能。为了优化这种情况,Java并发包(java.util.concurrent)提供了丰富的同步机制,其中“等待-通知”模式(Wait-Notify Mechanism)是一种非常有效且广泛使用的解决方案。 #### 一、理解“等待-通知”机制 “等待-通知”机制是Java中用于线程间通信的一种模式,它依赖于Object类中的`wait()`、`notify()`和`notifyAll()`三个方法来实现。这三个方法都是java.lang.Object类的成员,因此Java中的任何对象都可以作为同步锁和通信媒介。 - **wait()**:让当前线程进入等待状态(阻塞状态),直到其他线程调用此对象的`notify()`方法或`notifyAll()`方法。调用`wait()`方法之前,线程必须获得该对象的锁。调用`wait()`方法后,线程会释放这个锁,并进入等待队列中等待。 - **notify()**:唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,选择哪个线程唤醒是未定义的。调用`notify()`方法之前,线程也必须获得该对象的锁。 - **notifyAll()**:唤醒在此对象监视器上等待的所有线程。 #### 二、循环等待的问题 在不使用“等待-通知”机制的情况下,当线程需要等待某个条件满足时,可能会采用循环检查的方式来实现。这种方式虽然简单,但存在明显的问题: 1. **CPU资源浪费**:线程会不断地检查条件是否满足,即使条件不满足也会持续占用CPU资源。 2. **响应性差**:如果条件长时间不满足,线程将一直占用CPU,无法及时响应其他任务。 3. **效率低下**:在多个线程同时等待同一条件时,每个线程都在无意义地消耗资源,整体效率极低。 #### 三、使用“等待-通知”机制优化 通过“等待-通知”机制,我们可以有效地避免循环等待带来的问题。其基本思想是:当线程需要等待某个条件时,不是通过循环检查,而是调用`wait()`方法进入等待状态,并释放锁;当条件满足时,另一个线程会调用`notify()`或`notifyAll()`方法唤醒等待的线程,并重新竞争锁以继续执行。 ##### 示例场景:生产者-消费者问题 为了更直观地说明“等待-通知”机制的应用,我们以经典的生产者-消费者问题为例进行说明。 **场景描述**:有一个共享的资源(如缓冲区),生产者线程负责向其中生产数据,消费者线程负责从中消费数据。生产者必须在缓冲区未满时才能生产,消费者必须在缓冲区非空时才能消费。 **不使用“等待-通知”机制的实现**(简化版,仅作对比): ```java public class Buffer { private final int capacity; private int size = 0; private final List<Integer> items = new ArrayList<>(); public Buffer(int capacity) { this.capacity = capacity; } public synchronized void produce(int item) throws InterruptedException { while (size == capacity) { Thread.sleep(100); // 简单的等待方式,实际应用中应避免 } items.add(item); size++; } public synchronized int consume() throws InterruptedException { while (size == 0) { Thread.sleep(100); // 同样是简单的等待方式 } int item = items.remove(0); size--; return item; } } ``` **使用“等待-通知”机制的实现**: ```java public class Buffer { private final int capacity; private int size = 0; private final List<Integer> items = new ArrayList<>(); private final Object lock = new Object(); // 显式锁对象 public Buffer(int capacity) { this.capacity = capacity; } public void produce(int item) throws InterruptedException { synchronized (lock) { while (size == capacity) { lock.wait(); // 等待空间 } items.add(item); size++; lock.notifyAll(); // 通知可能在等待的消费者 } } public int consume() throws InterruptedException { synchronized (lock) { while (size == 0) { lock.wait(); // 等待数据 } int item = items.remove(0); size--; lock.notifyAll(); // 通知可能在等待的生产者 return item; } } } ``` 在这个优化后的实现中,我们使用了显式的锁对象`lock`来同步访问缓冲区和进行线程间的通信。当缓冲区满时,生产者线程会调用`lock.wait()`进入等待状态,并释放锁;当消费者消费了数据后,通过调用`lock.notifyAll()`唤醒所有等待的线程(包括生产者和消费者),但通常情况下,由于条件限制,只有生产者或消费者中的一个会被唤醒并继续执行。 #### 四、注意事项 1. **避免虚假唤醒**:`wait()`方法可能在没有被`notify()`或`notifyAll()`显式唤醒的情况下就返回了,这被称为虚假唤醒(Spurious Wakeup)。因此,在`wait()`的循环中应该总是重新检查条件是否满足。 2. **确保在同步块中调用**:`wait()`、`notify()`和`notifyAll()`方法必须在同步块或同步方法中调用,且这些方法的调用对象必须是同一个对象,即同步锁。 3. **使用`notifyAll()`而非`notify()`**:除非你有明确的理由知道只有一个线程会等待某个条件,否则通常应该使用`notifyAll()`来唤醒所有等待的线程,以避免潜在的死锁或活锁问题。 4. **确保线程安全**:在使用“等待-通知”机制时,必须确保对共享资源的访问是线程安全的,即所有对共享资源的修改都应该在同步块中进行。 #### 五、总结 通过“等待-通知”机制,我们可以有效地优化Java并发编程中的循环等待问题,提高系统的响应性和效率。在生产者-消费者等典型场景中,这一机制显得尤为重要。理解和掌握“等待-通知”机制是Java并发编程的基础,也是提升程序性能和稳定性的关键。
上一篇:
05 | 一不小心就死锁了,怎么办?
下一篇:
07 | 安全性、活跃性以及性能问题
该分类下的相关小册推荐:
SpringBoot合辑-高级篇
Java语言基础13-类的加载和反射
Mybatis合辑1-Mybatis基础入门
Mybatis合辑4-Mybatis缓存机制
java源码学习笔记
JAVA 函数式编程入门与实践
Java语言基础7-Java中的异常
Java高并发秒杀入门与实战
Java语言基础10-Java中的集合
经典设计模式Java版
SpringBoot合辑-初级篇
Java必知必会-JDBC