synchronized的wait/notify机制详解与实战应用

一、wait/notify机制的基本原理

wait/notify是Java多线程编程中最基础的线程间通信机制,它们都是Object类的方法而非Thread类的方法,这意味着任何Java对象都可以作为线程间通信的媒介。这套机制的核心思想是通过对象监视器(Monitor)实现线程的等待与唤醒。

基本工作原理​:

  • wait()​:使当前线程进入等待状态,并释放对象锁
  • notify()​:随机唤醒一个在该对象上等待的线程
  • notifyAll()​:唤醒所有在该对象上等待的线程

这三个方法必须配合synchronized使用,因为它们依赖于对象的内置锁(监视器锁)。调用这些方法时,当前线程必须持有该对象的锁,否则会抛出IllegalMonitorStateException异常。

二、wait/notify的核心方法详解

1. wait方法系列

  • wait():使当前线程等待,直到其他线程调用此对象的notify()或notifyAll()方法
  • wait(long timeout):带超时时间的等待,单位毫秒
  • wait(long timeout, int nanos):更精确的超时控制,可指定纳秒

2. notify与notifyAll

  • notify():唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,则选择其中一个进行唤醒(选择是任意的)
  • notifyAll():唤醒在此对象监视器上等待的所有线程

关键区别​:

  • notify()仅唤醒一个线程,可能导致线程饥饿问题
  • notifyAll()唤醒所有线程,确保不会有线程被遗漏,但会带来更大的性能开销

三、wait/notify的正确使用方式

1. 基本使用规则

  1. 必须在synchronized同步块或方法中调用
  2. 调用wait/notify的对象必须与synchronized锁定的对象相同
  3. wait后必须使用循环检查等待条件(避免虚假唤醒)

2. 避免虚假唤醒

Java官方文档明确指出:"线程可能在没有被通知、中断或超时的情况下被唤醒,这被称为虚假唤醒"。因此,等待应该总是发生在循环中。

正确写法​:

csharp 复制代码
synchronized (lock) {
    while (!condition) {  // 使用while而非if
        lock.wait();
    }
    // 条件满足后的处理
}

3. 锁的释放与获取

  • 调用wait()会立即释放锁,使其他线程可以获取锁
  • 调用notify()/notifyAll()不会立即释放锁,只有在同步块执行完毕后才会释放
  • 被唤醒的线程需要重新竞争锁才能继续执行

四、经典应用场景与实战示例

1. 生产者-消费者模式

这是wait/notify最典型的应用场景,解决生产者和消费者速度不匹配的问题。

基础实现​:

arduino 复制代码
public class WaitNotifyExample {
    private final Queue<String> queue = new LinkedList<>();
    private final int MAX_SIZE = 5;
    
    public synchronized void produce(String data) throws InterruptedException {
        while (queue.size() == MAX_SIZE) {
            wait(); // 队列满,生产者等待
        }
        queue.add(data);
        notify(); // 唤醒消费者
    }
    
    public synchronized String consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // 队列空,消费者等待
        }
        String data = queue.poll();
        notify(); // 唤醒生产者
        return data;
    }
}

多生产者多消费者场景​:

在多生产者多消费者场景下,应改用notifyAll()避免线程饥饿:

arduino 复制代码
public synchronized void produceMulti(String data) throws InterruptedException {
    while (queue.size() == MAX_SIZE) {
        wait();
    }
    queue.add(data);
    notifyAll(); // 必须用notifyAll确保正确唤醒
}

2. 线程交替执行

wait/notify可用于控制线程按特定顺序交替执行,如交替打印数字和字母:

csharp 复制代码
public class AlternatePrint {
    private final Object lock = new Object();
    private boolean flag = true; // 控制交替的标志
    
    public void printNum() throws InterruptedException {
        synchronized (lock) {
            for (int i = 1; i <= 26; i++) {
                while (!flag) {
                    lock.wait();
                }
                System.out.print(i);
                flag = false;
                lock.notify();
            }
        }
    }
    
    public void printChar() throws InterruptedException {
        synchronized (lock) {
            for (char c = 'A'; c <= 'Z'; c++) {
                while (flag) {
                    lock.wait();
                }
                System.out.print(c + " ");
                flag = true;
                lock.notify();
            }
        }
    }
}

五、wait/notify的高级应用与注意事项

1. 通知过早问题

如果notify()在wait()之前调用,可能导致等待线程永远无法被唤醒。解决方案是添加状态标志:

csharp 复制代码
public class EarlyNotifyExample {
    static boolean isFirst = true; // 是否第一个运行的线程标志
    
    public static void main(String[] args) {
        final Object lock = new Object();
        
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                while (isFirst) {
                    try {
                        System.out.println("wait begin");
                        lock.wait();
                        System.out.println("wait end");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("notify begin");
                lock.notify();
                System.out.println("notify end");
                isFirst = false;
            }
        });
        
        t1.start();
        t2.start();
    }
}

2. wait等待条件变化

当多个线程等待同一条件时,条件变化后应确保所有相关线程被正确通知:

csharp 复制代码
public class ConditionChangeExample {
    static List list = new ArrayList<>();
    
    static public void subtract() {
        synchronized (list) {
            while (list.size() == 0) {
                try {
                    System.out.println(Thread.currentThread().getName()+" wait begin");
                    list.wait();
                    System.out.println(Thread.currentThread().getName()+" wait end");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove(0);
        }
    }
}

3. 死锁预防

使用带超时的wait(long timeout)可以避免永久等待导致的死锁:

kotlin 复制代码
// 超时等待,避免永久死锁
if (!condition) {
    this.wait(1000); // 最多等待1秒
}

六、wait/notify与Condition的对比

Java 5引入了Condition接口,提供了比wait/notify更灵活的线程间控制机制:

特性 wait/notify Condition
关联锁 必须与synchronized配合使用 必须与Lock配合使用
等待队列 一个对象一个等待队列 一个Lock可创建多个Condition
精确通知 不支持 支持分组唤醒
超时控制 有限支持 更灵活的超时机制
中断响应 只能抛出InterruptedException 提供可中断和不可中断的等待

Condition优势场景​:

  • 需要精确唤醒特定类型线程(如只唤醒生产者或消费者)
  • 需要更灵活的超时控制
  • 需要实现公平锁机制

七、最佳实践与常见陷阱

1. 最佳实践

  1. 始终在循环中调用wait()​:防止虚假唤醒
  2. 优先使用notifyAll()​:除非能确保notify()不会导致线程饥饿
  3. 保持同步块简短:减少锁持有时间,提高并发性
  4. 正确处理中断:恢复中断状态,不吞掉中断

2. 常见陷阱

  1. 在非同步块中调用wait/notify:导致IllegalMonitorStateException
  2. 使用不同对象的wait/notify:无法实现线程间通信
  3. 忽略虚假唤醒:使用if而非while检查条件
  4. notify过早:在wait之前调用notify导致信号丢失
  5. 过度使用notifyAll:在明确知道只需唤醒一个线程时使用notifyAll会造成性能浪费

八、性能考量与替代方案

1. 性能优化

  1. 减小同步范围:只同步必要的代码块
  2. 分离读写锁:读多写少场景使用ReadWriteLock
  3. 使用并发集合:如ConcurrentHashMap替代同步的HashMap

2. 高级替代方案

  1. CountDownLatch:主线程等待多个子任务完成
  2. CyclicBarrier:线程分阶段协同工作
  3. CompletableFuture:Java 8引入的异步任务编排
  4. BlockingQueue:内置阻塞机制的线程安全队列

wait/notify作为Java最基础的线程间通信机制,理解其原理和正确使用方式对于编写可靠的多线程程序至关重要。虽然Java并发包提供了更多高级同步工具,但在许多场景下,wait/notify仍然是简单有效的解决方案。

相关推荐
努力的小雨2 小时前
CodeBuddy CLI工具深度测评:从零到一实现鸿蒙游戏开发实践
后端
文心快码BaiduComate2 小时前
北京互联网大会 | 百度副总裁陈洋:AI Coding为新质生产力注入“新码力”
前端·后端·程序员
yk100104 小时前
Spring属性配置解析机制详解
java·后端·spring
紫穹4 小时前
Qwen Code CLI:让命令行直接听懂人话
后端
小虎l4 小时前
Java并发编程原理精讲
后端
谁黑皮谁肘击谁在连累直升机4 小时前
for循环的了解与应用
前端·后端
yinke小琪4 小时前
什么?上班五年还不清楚SafePoint?JVM的“安全点”揭秘
java·后端·面试
野犬寒鸦4 小时前
今日面试之快问快答:Redis篇
java·数据库·redis·后端·缓存·面试·职场和发展