杰瑞科技汇

Java中notify和wait的正确使用姿势是什么?

Of course! Let's break down notify() and wait() in Java. They are fundamental tools for writing multi-threaded applications, but they are often misunderstood and misused.

Java中notify和wait的正确使用姿势是什么?-图1
(图片来源网络,侵删)

The Core Concept: Thread Communication

Imagine you have two threads: a Producer and a Consumer.

  • The Producer creates data and puts it into a shared buffer.
  • The Consumer takes data from the shared buffer to process it.

What happens if the buffer is empty? The Consumer can't do anything. It should wait until the Producer adds data. What happens if the buffer is full? The Producer can't add more data. It should wait until the Consumer takes some data away.

wait() and notify() are the mechanisms that allow these threads to communicate with each other and coordinate their actions efficiently, without wasting CPU cycles.


The Golden Rule: Always Use synchronized

wait() and notify() are not magic methods. They must be called from within a synchronized block or method. This is crucial for two reasons:

Java中notify和wait的正确使用姿势是什么?-图2
(图片来源网络,侵删)
  1. Mutual Exclusion: It ensures that only one thread can manipulate the shared resource (the buffer) at a time, preventing race conditions.
  2. Memory Visibility: It guarantees that changes made to shared variables by one thread are visible to other threads when they acquire the same lock.

The wait() Method

When a thread calls wait() on an object, it does the following:

  1. It releases the lock it holds on that object.
  2. It enters the wait set of that object.
  3. It suspends execution and waits until one of three things happens:
    • Another thread calls notify() or notifyAll() on the same object.
    • Another thread calls interrupt() on the waiting thread.
    • The specified waiting time (if you use wait(long timeout)) has elapsed.

Key takeaway: wait() releases the lock, allowing other threads to enter the synchronized block and potentially call notify().

The notify() Method

When a thread calls notify() on an object, it does the following:

  1. It wakes up one (arbitrarily chosen) thread from the wait set of that object.
  2. The awakened thread then attempts to re-acquire the lock on the object. It will block until the current thread (the one that called notify) releases the lock.

Key takeaway: notify() wakes up one waiting thread. It does not release the lock immediately. The lock is released when the synchronized block/method exits.

Java中notify和wait的正确使用姿势是什么?-图3
(图片来源网络,侵删)

notifyAll() vs. notify()

  • notify(): Wakes up one random thread from the wait set. Use this when you know that any of the waiting threads can proceed and you want to minimize context switching.
  • notifyAll(): Wakes up all threads waiting on that object. All awakened threads will then compete to re-acquire the lock. Only one will succeed; the others will go back to waiting. Use this when you don't know which thread should proceed or when a condition change is relevant to all waiting threads.

The Classic Example: Producer-Consumer

This is the best way to understand how they work together.

The Shared Resource (Buffer)

This class will hold the data and the synchronized methods.

public class Buffer {
    private int[] buffer;
    private int count; // Number of items in the buffer
    private int in = 0; // Index for the producer to put an item
    private int out = 0; // Index for the consumer to take an item
    private final int size;
    public Buffer(int size) {
        this.size = size;
        buffer = new int[size];
        this.count = 0;
    }
    // Called by the Producer
    public synchronized void put(int item) throws InterruptedException {
        // Wait if the buffer is full
        while (count == size) {
            System.out.println("Producer: Buffer is full. Waiting...");
            wait(); // Releases the lock and waits
        }
        buffer[in] = item;
        in = (in + 1) % size;
        count++;
        System.out.println("Producer: Produced " + item + ". Buffer count: " + count);
        // Notify a waiting consumer that a new item is available
        notify(); // Wake up one consumer
    }
    // Called by the Consumer
    public synchronized int get() throws InterruptedException {
        // Wait if the buffer is empty
        while (count == 0) {
            System.out.println("Consumer: Buffer is empty. Waiting...");
            wait(); // Releases the lock and waits
        }
        int item = buffer[out];
        out = (out + 1) % size;
        count--;
        System.out.println("Consumer: Consumed " + item + ". Buffer count: " + count);
        // Notify a waiting producer that there is space
        notify(); // Wake up one producer
        return item;
    }
}

Why while loop and not if? This is a critical point called the "spurious wakeup" problem. A thread can sometimes wake up from wait() even if no notify() was called for it. This is a rare, platform-specific behavior. By using a while loop, we re-check the condition (count == size or count == 0) after waking up. If the condition is still true, the thread will simply call wait() again. This makes the code robust.

The Producer Thread

public class Producer implements Runnable {
    private Buffer buffer;
    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }
    @Override
    public void run() {
        try {
            for (int i = 1; i <= 10; i++) {
                buffer.put(i);
                Thread.sleep(100); // Simulate time taken to produce
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

The Consumer Thread

public class Consumer implements Runnable {
    private Buffer buffer;
    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }
    @Override
    public void run() {
        try {
            for (int i = 1; i <= 10; i++) {
                buffer.get();
                Thread.sleep(200); // Simulate time taken to consume
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

The Main Class

public class Main {
    public static void main(String[] args) {
        Buffer buffer = new Buffer(3); // A small buffer to easily see waiting
        Thread producerThread = new Thread(new Producer(buffer));
        Thread consumerThread = new Thread(new Consumer(buffer));
        producerThread.start();
        consumerThread.start();
        try {
            producerThread.join();
            consumerThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main: Finished.");
    }
}

Sample Output

You'll see an output like this, demonstrating the waiting and notification:

Producer: Produced 1. Buffer count: 1
Consumer: Consumed 1. Buffer count: 0
Producer: Produced 2. Buffer count: 1
Producer: Produced 3. Buffer count: 2
Producer: Produced 4. Buffer count: 3
Producer: Produced 5. Buffer count: 3
Producer: Buffer is full. Waiting...
Consumer: Consumed 2. Buffer count: 2
Producer: Produced 5. Buffer count: 3
Producer: Buffer is full. Waiting...
Consumer: Consumed 3. Buffer count: 2
Producer: Produced 6. Buffer count: 3
Producer: Buffer is full. Waiting...
... and so on

Modern Alternatives (The java.util.concurrent Package)

While wait() and notify() are foundational, they are error-prone. The modern Java concurrency library provides safer and higher-level abstractions.

  1. BlockingQueue: This is the recommended solution for the Producer-Consumer problem. It's a thread-safe queue that handles all the waiting and notifying for you.

    // Example using ArrayBlockingQueue
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    public class ModernProducerConsumer {
        public static void main(String[] args) {
            // The queue has a capacity of 10
            BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
            Thread producer = new Thread(() -> {
                try {
                    for (int i = 1; i <= 50; i++) {
                        queue.put(i); // Blocks if the queue is full
                        System.out.println("Produced " + i);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
            Thread consumer = new Thread(() -> {
                try {
                    while (true) {
                        Integer item = queue.take(); // Blocks if the queue is empty
                        System.out.println("Consumed " + item);
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
            producer.start();
            consumer.start();
        }
    }
  2. java.util.concurrent.locks: This package provides more advanced locking mechanisms like ReentrantLock and Condition objects. Condition objects are essentially more powerful and flexible versions of wait/notify.

Summary

Feature wait() / notify() BlockingQueue / Locks
Complexity Low-level, error-prone. High-level, safer.
Control Fine-grained control over threads. Abstracts away thread management.
Use Case Learning concurrency fundamentals, very specific custom scenarios. Most common concurrent tasks (Producer-Consumer, task pools, etc.).
Recommendation Understand them deeply, but prefer java.util.concurrent in production code. The standard and recommended approach for new code.
分享:
扫描分享到社交APP
上一篇
下一篇