跳转至

Java 中的 notify()wait():线程间通信的关键机制

简介

在多线程编程中,线程间的有效通信至关重要。Java 提供了 notify()wait() 方法来实现线程间的协作与同步。通过这两个方法,线程可以在特定条件下暂停执行(wait()),并在条件满足时被唤醒(notify()),从而实现复杂的并发控制逻辑。

目录

  1. 基础概念
    • notify() 方法
    • wait() 方法
    • 监视器(Monitor)
  2. 使用方法
    • wait() 的调用
    • notify() 的调用
    • notifyAll() 的调用
  3. 常见实践
    • 生产者 - 消费者模型
    • 线程间的条件同步
  4. 最佳实践
    • 避免死锁
    • 合理使用 notify()notifyAll()
    • 正确处理 InterruptedException
  5. 小结
  6. 参考资料

基础概念

notify() 方法

notify()Object 类的一个方法,用于唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,则会随机选择一个线程被唤醒。被唤醒的线程将进入可运行状态,但并不一定会立即执行,具体执行时机由操作系统的线程调度器决定。

wait() 方法

wait() 同样是 Object 类的方法,它会使当前线程等待,直到其他线程调用该对象的 notify()notifyAll() 方法。在等待期间,线程会释放对象的监视器,允许其他线程获取并访问该对象。这是实现线程同步和协作的关键机制。

监视器(Monitor)

在 Java 中,每个对象都有一个关联的监视器(也称为内置锁)。当一个线程访问被 synchronized 关键字修饰的代码块或方法时,它会自动获取对象的监视器。只有获取到监视器的线程才能执行相应的代码,其他线程则需要等待。wait()notify()notifyAll() 方法的调用必须在获取对象监视器的前提下进行,否则会抛出 IllegalMonitorStateException

使用方法

wait() 的调用

public class WaitExample {
    public static void main(String[] args) {
        Object lock = new Object();

        Thread waitingThread = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("等待线程开始等待...");
                    lock.wait();
                    System.out.println("等待线程被唤醒...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        waitingThread.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread notifyingThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("通知线程开始通知...");
                lock.notify();
            }
        });

        notifyingThread.start();
    }
}

在上述代码中,waitingThread 线程进入 synchronized 块后调用 lock.wait(),此时它会释放 lock 的监视器并进入等待状态。notifyingThread 线程在延迟 2 秒后启动,进入 synchronized 块并调用 lock.notify(),唤醒 waitingThread 线程。waitingThread 被唤醒后会重新获取 lock 的监视器,继续执行后续代码。

notify() 的调用

如上述代码所示,notify() 方法通常在另一个线程中调用,用于唤醒等待在同一对象监视器上的单个线程。调用 notify() 时,必须确保当前线程已经获取了对象的监视器,否则会抛出异常。

notifyAll() 的调用

notifyAll() 方法会唤醒在此对象监视器上等待的所有线程。与 notify() 不同,notifyAll() 会让所有等待的线程都有机会竞争获取对象的监视器并继续执行。

public class NotifyAllExample {
    public static void main(String[] args) {
        Object lock = new Object();

        Thread waitingThread1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("等待线程 1 开始等待...");
                    lock.wait();
                    System.out.println("等待线程 1 被唤醒...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread waitingThread2 = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("等待线程 2 开始等待...");
                    lock.wait();
                    System.out.println("等待线程 2 被唤醒...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        waitingThread1.start();
        waitingThread2.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Thread notifyingThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("通知线程开始通知所有线程...");
                lock.notifyAll();
            }
        });

        notifyingThread.start();
    }
}

在这个例子中,waitingThread1waitingThread2 都在等待 lock 的通知。notifyingThread 调用 lock.notifyAll() 后,两个等待线程都会被唤醒并竞争获取 lock 的监视器。

常见实践

生产者 - 消费者模型

生产者 - 消费者模型是多线程编程中的经典场景,通过 notify()wait() 可以很方便地实现。生产者线程生成数据并放入缓冲区,消费者线程从缓冲区中取出数据进行处理。当缓冲区满时,生产者线程需要等待;当缓冲区为空时,消费者线程需要等待。

import java.util.LinkedList;
import java.util.Queue;

class ProducerConsumer {
    private final Queue<Integer> buffer = new LinkedList<>();
    private final int capacity = 5;

    public void produce() throws InterruptedException {
        while (true) {
            synchronized (this) {
                while (buffer.size() == capacity) {
                    System.out.println("缓冲区已满,生产者等待...");
                    wait();
                }
                int item = (int) (Math.random() * 100);
                buffer.add(item);
                System.out.println("生产者生产了: " + item);
                notify();
            }
        }
    }

    public void consume() throws InterruptedException {
        while (true) {
            synchronized (this) {
                while (buffer.isEmpty()) {
                    System.out.println("缓冲区为空,消费者等待...");
                    wait();
                }
                int item = buffer.poll();
                System.out.println("消费者消费了: " + item);
                notify();
            }
        }
    }
}

public class ProducerConsumerExample {
    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        Thread producerThread = new Thread(() -> {
            try {
                pc.produce();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumerThread = new Thread(() -> {
            try {
                pc.consume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producerThread.start();
        consumerThread.start();
    }
}

在上述代码中,ProducerConsumer 类包含一个缓冲区和 produce()consume() 方法。produce() 方法在缓冲区满时调用 wait() 等待,生产数据后调用 notify() 通知消费者;consume() 方法在缓冲区为空时调用 wait() 等待,消费数据后调用 notify() 通知生产者。

线程间的条件同步

有时候,线程需要在特定条件满足时才继续执行。通过 wait()notify() 可以实现这种条件同步。

public class ConditionSyncExample {
    private static boolean conditionMet = false;

    public static void main(String[] args) {
        Object lock = new Object();

        Thread waitingThread = new Thread(() -> {
            synchronized (lock) {
                while (!conditionMet) {
                    try {
                        System.out.println("等待线程等待条件满足...");
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("等待线程条件满足,继续执行...");
            }
        });

        Thread notifyingThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("通知线程设置条件并通知...");
                conditionMet = true;
                lock.notify();
            }
        });

        waitingThread.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        notifyingThread.start();
    }
}

在这个例子中,waitingThread 线程在 conditionMetfalse 时调用 wait() 等待,notifyingThread 线程在延迟 2 秒后设置 conditionMettrue 并调用 notify() 唤醒 waitingThread

最佳实践

避免死锁

死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放资源时就会发生死锁。为了避免死锁,应遵循以下原则: - 尽量减少锁的嵌套使用。 - 确保线程获取锁的顺序一致。 - 使用 tryLock() 方法替代 lock() 方法,避免无限期等待。

合理使用 notify()notifyAll()

  • notify() 适用于唤醒单个等待线程的场景,当只有一个线程需要被唤醒时,使用 notify() 可以提高效率。
  • notifyAll() 适用于多个等待线程都需要被唤醒的场景,例如在生产者 - 消费者模型中,当缓冲区状态发生变化时,可能需要唤醒所有等待的生产者或消费者线程。

正确处理 InterruptedException

wait() 方法会抛出 InterruptedException,在调用 wait() 时必须正确处理该异常。通常情况下,应在捕获异常后清理资源并终止线程的执行。

synchronized (lock) {
    try {
        lock.wait();
    } catch (InterruptedException e) {
        // 清理资源
        // 终止线程
        Thread.currentThread().interrupt();
    }
}

小结

notify()wait() 是 Java 多线程编程中实现线程间通信和同步的重要方法。通过合理使用这两个方法,可以实现复杂的并发控制逻辑,如生产者 - 消费者模型和线程间的条件同步。在使用过程中,需要注意避免死锁、合理选择 notify()notifyAll(),并正确处理 InterruptedException。掌握这些知识和技巧,将有助于编写高效、稳定的多线程程序。

参考资料

  • 《Effective Java》,Joshua Bloch
  • 《Java 并发编程实战》,Brian Goetz 等