1. 为什么我们需要等待通知机制
当多个线程需要协作完成某项任务时,单纯的互斥锁往往力不从心。想象一个典型的生产者-消费者场景:生产者线程往队列里放数据,消费者线程从队列取数据。如果只用synchronized关键字,代码可能会写成这样:
java复制// 错误示范:忙等待
while(queue.isEmpty()) {
// 空转等待
}
Object item = queue.poll();
这种实现方式存在严重问题——消费者线程会持续占用CPU进行无意义的循环检查(忙等待)。在实际项目中,这种写法会导致CPU利用率飙升,而真正有用的工作却进展缓慢。
等待通知机制的核心价值在于:
- 资源节约:等待状态的线程不会消耗CPU资源
- 协作效率:精确控制线程的唤醒时机,避免无效竞争
- 响应速度:事件触发后能立即唤醒相关线程
重要提示:在Java中,每个对象都内置了一个等待队列(wait set)和一个条件队列(condition queue),这是实现等待通知机制的底层基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. wait/notify的工作原理与正确用法
2.1 对象监视器模型
Java中的每个对象都关联着一个监视器(monitor),包含:
- 一个互斥锁(mutex lock)
- 一个等待集合(wait set)
- 一个入口集合(entry set)
java复制public class WaitNotifyExample {
private final Object lock = new Object();
private boolean condition = false;
public void waitForCondition() throws InterruptedException {
synchronized (lock) {
while(!condition) {
lock.wait(); // 释放锁并进入WAITING状态
}
// 条件满足后继续执行
}
}
public void signalCondition() {
synchronized (lock) {
condition = true;
lock.notifyAll(); // 唤醒所有等待线程
}
}
}
2.2 使用时的三个黄金法则
-
必须在同步块中调用:wait()/notify()必须持有对象监视器锁,否则会抛出IllegalMonitorStateException
-
总是使用循环检查条件:被唤醒后要重新检查条件,因为:
- 可能有多个线程在等待同一条件
- 存在虚假唤醒(spurious wakeup)的可能性
-
优先使用notifyAll():notify()随机唤醒一个线程,容易导致信号丢失;notifyAll()更安全但开销略大
2.3 常见陷阱与解决方案
陷阱1:丢失唤醒(Missed Wakeup)
java复制// 线程A
synchronized(lock) {
condition = true;
lock.notify();
}
// 线程B
synchronized(lock) {
if(!condition) {
lock.wait(); // 可能永远等待
}
}
解决方案:确保检查和wait()是原子操作
陷阱2:嵌套锁中的死锁
java复制synchronized(lock1) {
synchronized(lock2) {
lock1.wait(); // 会释放lock1但保持lock2
}
}
解决方案:避免在持有多个锁时调用wait()
3. Condition接口的进阶用法
Java 1.5引入的Lock框架提供了更灵活的Condition接口:
java复制Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// 等待方
lock.lock();
try {
while(!conditionMet) {
condition.await();
}
} finally {
lock.unlock();
}
// 通知方
lock.lock();
try {
conditionMet = true;
condition.signal();
} finally {
lock.unlock();
}
Condition的优势:
- 一个Lock可以创建多个Condition实例
- 支持可中断/不可中断的等待
- 提供定时等待功能(awaitNanos()等)
典型应用场景:有界阻塞队列
java复制public class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
4. 线程状态转换与调度机制
4.1 完整的线程生命周期
- NEW:刚创建未启动
- RUNNABLE:可运行(可能在等待CPU)
- BLOCKED:等待获取监视器锁
- WAITING:无限期等待(wait()/join())
- TIMED_WAITING:限期等待(sleep()/wait(timeout))
- TERMINATED:执行完成
mermaid复制stateDiagram-v2
[*] --> NEW
NEW --> RUNNABLE: start()
RUNNABLE --> BLOCKED: 等待synchronized锁
BLOCKED --> RUNNABLE: 获取到锁
RUNNABLE --> WAITING: wait()/join()
WAITING --> RUNNABLE: notify()/notifyAll()
RUNNABLE --> TIMED_WAITING: sleep()/wait(timeout)
TIMED_WAITING --> RUNNABLE: 超时/被唤醒
RUNNABLE --> TERMINATED: run()结束
4.2 线程调度的关键点
- 优先级只是提示:Java线程优先级(1-10)不保证执行顺序
- yield()的局限性:只是让出当前CPU时间片,可能立即又被调度
- 守护线程特性:当所有非守护线程结束时,JVM会自动退出
实战经验:在Linux系统上,Java线程优先级会被映射到不同的nice值,但最终调度仍由操作系统决定。不要依赖优先级来实现业务逻辑。
5. 高并发场景下的性能优化
5.1 锁优化技巧
-
减小锁粒度:
- 用ConcurrentHashMap代替synchronized Map
- 分离锁(如读写锁)
-
锁消除:
java复制public String concat(String s1, String s2) { StringBuffer sb = new StringBuffer(); // 局部变量,可消除同步 sb.append(s1); sb.append(s2); return sb.toString(); } -
锁粗化:
java复制// 优化前 for(int i=0; i<100; i++) { synchronized(lock) { // 操作 } } // 优化后 synchronized(lock) { for(int i=0; i<100; i++) { // 操作 } }
5.2 线程池参数调优
以ThreadPoolExecutor为例,关键参数关系:
code复制corePoolSize ≤ maximumPoolSize
keepAliveTime ≥ 0
workQueue容量影响拒绝策略触发
配置建议:
- CPU密集型:corePoolSize = CPU核心数 + 1
- IO密集型:corePoolSize = CPU核心数 × 2
- 混合型:拆分不同线程池处理
java复制ExecutorService executor = new ThreadPoolExecutor(
4, // corePoolSize
8, // maximumPoolSize
60, TimeUnit.SECONDS, // keepAliveTime
new ArrayBlockingQueue<>(100), // workQueue
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
5.3 避免常见性能陷阱
- 上下文切换开销:线程数不是越多越好,建议通过压测找到最优值
- 缓存一致性代价:volatile变量会禁用CPU缓存,非必要不使用
- 对象分配压力:频繁创建线程会触发GC,应使用线程池
实测案例:一个简单的HTTP服务,在不同线程数下的QPS对比:
code复制线程数 | 平均响应时间(ms) | QPS
---------------------------------
4 | 45 | 2200
8 | 25 | 3800
16 | 22 | 4200
32 | 30 | 3300 // 开始下降
64 | 55 | 1800 // 明显恶化
6. 现代并发模型的发展
6.1 协程与虚拟线程
Java 19引入的虚拟线程(Project Loom)特点:
- 轻量级:一个平台线程可承载数千个虚拟线程
- 低成本:上下文切换由JVM调度而非操作系统
- 兼容性:保持原有Thread API不变
java复制try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
} // 自动等待所有任务完成
6.2 响应式编程模型
Reactor模式示例(使用Project Reactor):
java复制Flux.range(1, 10)
.parallel() // 并行处理
.runOn(Schedulers.parallel())
.map(i -> intensiveCalculation(i))
.sequential()
.subscribe(System.out::println);
6.3 结构化并发(Java 19预览)
解决"线程泄漏"问题的新范式:
java复制try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> findUser());
Future<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // 等待两者完成
scope.throwIfFailed(); // 检查异常
return new Response(user.resultNow(), order.resultNow());
} // 自动取消未完成的任务
在实际项目中,我发现很多开发者在处理并发问题时容易陷入两个极端:要么过度同步导致性能瓶颈,要么过度乐观引发竞态条件。正确的做法是:
- 先用简单的同步方案实现正确性
- 通过性能测试找出真正的热点
- 针对热点进行精细化并发控制
- 使用jstack、JFR等工具验证优化效果
对于等待通知机制,特别要注意在复杂业务场景中,多个条件变量之间的交互可能导致微妙的死锁问题。建议在代码审查时特别关注:
- wait()是否在循环中调用
- notify/notifyAll的使用是否恰当
- 锁的获取和释放是否成对出现
- 是否存在嵌套锁的情况
