杰瑞科技汇

Java中wait/notify如何正确使用?

Of course! Let's dive deep into wait() and notify() in Java. These are fundamental but often misunderstood methods for communication between threads.

Java中wait/notify如何正确使用?-图1
(图片来源网络,侵删)

The Core Idea: Why Do We Need Them?

Imagine a scenario where one thread (the Producer) is creating data and another thread (the Consumer) is processing that data. The Consumer can't process data if there isn't any, and the Producer shouldn't create more data if the buffer is full.

  • Without wait()/notify(), the Consumer might waste CPU cycles by constantly checking "is there data yet?" in a loop (this is called busy-waiting).
  • We need a way for the Consumer to go to sleep when there's no data and be woken up by the Producer when new data arrives.

wait() and notify() are the tools Java provides for this kind of efficient thread communication. They allow threads to pause and resume execution based on specific conditions, without consuming CPU resources.


Key Concepts and Rules

Before we see the code, you must understand these critical rules:

  1. They Can Only Be Called Inside a Synchronized Block/Method: This is the most important rule. wait() and notify() are not instance methods you can call on any object. They must be called from within a context where the current thread holds the object's monitor lock (or intrinsic lock). This prevents race conditions.

    Java中wait/notify如何正确使用?-图2
    (图片来源网络,侵删)
  2. The Purpose of wait():

    • It tells the current thread to release the monitor lock and pause execution.
    • The thread then enters the waiting set for that specific object.
    • It will remain there until one of three things happens: a. Another thread calls notify() or notifyAll() on the same object. b. Another thread calls interrupt() on the waiting thread. c. It's "spuriously" awakened (rare, but you should always wait in a loop).
  3. The Purpose of notify():

    • It wakes up one single thread from the waiting set of the object.
    • The awakened thread then attempts to re-acquire the monitor lock. It will block until it can get the lock.
    • Crucially: notify() does not release the lock immediately. The thread that called notify() must release the lock (by exiting its synchronized block) before the awakened thread can proceed.
  4. The Purpose of notifyAll():

    • It wakes up all threads waiting on the object's monitor.
    • Just like notify(), they will all compete to re-acquire the lock. Only one will succeed.
  5. Always Wait in a Loop (The "Guarded Suspension" Pattern):

    • Never call wait() outside of a while loop that checks the condition.
    • Why? A thread can be awakened for reasons other than notify() (a "spurious wakeup"). The loop ensures that if the condition is still not met after waking up, the thread will simply call wait() again.

The Classic Producer-Consumer Example

Let's build a simple thread-safe bounded buffer. The Buffer can hold only one item.

The Shared Buffer Class

This class will be accessed by both the Producer and Consumer threads.

public class Buffer {
    private int item;
    private boolean isEmpty = true; // Condition flag
    // Called by the Producer to put an item in the buffer
    public synchronized void put(int item) throws InterruptedException {
        // Wait while the buffer is NOT empty
        // The loop is crucial to handle spurious wakeups
        while (!isEmpty) {
            System.out.println("Producer: Buffer is full. Waiting...");
            wait(); // Releases lock and waits
        }
        // If we get here, the buffer is empty. Produce the item.
        this.item = item;
        isEmpty = false;
        System.out.println("Producer: Produced " + item);
        // Wake up the Consumer thread
        notify(); // Notifies the thread waiting on THIS object's monitor
    }
    // Called by the Consumer to get an item from the buffer
    public synchronized int get() throws InterruptedException {
        // Wait while the buffer IS empty
        while (isEmpty) {
            System.out.println("Consumer: Buffer is empty. Waiting...");
            wait(); // Releases lock and waits
        }
        // If we get here, the buffer has data. Consume the item.
        isEmpty = true;
        System.out.println("Consumer: Consumed " + item);
        // Wake up the Producer thread
        notify(); // Notifies the thread waiting on THIS object's monitor
        return this.item;
    }
}

The Producer and Consumer Threads

public class Producer implements Runnable {
    private final Buffer buffer;
    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            try {
                buffer.put(i);
                Thread.sleep(500); // Simulate production time
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}
public class Consumer implements Runnable {
    private final Buffer buffer;
    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            try {
                buffer.get();
                Thread.sleep(1000); // Simulate consumption time (slower)
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

The Main Class to Run It

public class Main {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();
        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: Program finished.");
    }
}

Expected Output

Notice the alternating pattern. The Producer waits, then the Consumer waits, and they wake each other up.

Producer: Produced 1
Consumer: Consumed 1
Producer: Produced 2
Consumer: Consumed 2
Producer: Produced 3
Consumer: Consumed 3
Producer: Produced 4
Consumer: Consumed 4
Producer: Produced 5
Consumer: Consumed 5
Main: Program finished.

wait() vs. sleep()

This is a very common point of confusion.

Feature wait() sleep()
Purpose For inter-thread communication. Pauses a thread until another thread signals it. To pause a thread for a specific amount of time.
Locking Releases the object's monitor lock. Does not release any locks.
Location Must be called inside a synchronized method or block. Can be called anywhere.
Wake-up By notify(), notifyAll(), or timeout. By the expiration of the timeout.
Throws IllegalMonitorStateException if not in a synchronized block. InterruptedException if interrupted.

Modern Alternatives: java.util.concurrent

While wait() and notify() are essential to understand for interviews and low-level control, modern Java development favors the high-level concurrency utilities in java.util.concurrent. They are safer, easier to use, and less error-prone.

For the Producer-Consumer problem, the best modern tool is the BlockingQueue.

Example with BlockingQueue

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ModernProducerConsumer {
    public static void main(String[] args) {
        // A thread-safe bounded buffer that can hold 1 item
        BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(1);
        Thread producerThread = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("Producer: Trying to put " + i);
                    queue.put(i); // Blocks if the queue is full
                    System.out.println("Producer: Successfully put " + i);
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        Thread consumerThread = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("Consumer: Trying to get...");
                    Integer item = queue.take(); // Blocks if the queue is empty
                    System.out.println("Consumer: Successfully got " + item);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        producerThread.start();
        consumerThread.start();
        try {
            producerThread.join();
            consumerThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The BlockingQueue handles all the complex wait()/notify() logic internally, providing a clean and simple API (put() and take()). This is the recommended approach for most real-world applications.

Summary

Method What it Does Key Points
wait() Pauses the current thread and releases the lock. Must be in a synchronized block. Always use in a while loop.
notify() Wakes up one random thread waiting on the same object's monitor. Does not release the lock. The notifying thread must exit the synchronized block first.
notifyAll() Wakes up all threads waiting on the same object's monitor. All awakened threads compete for the lock. Use when any waiting thread can proceed.
sleep() Pauses the current thread for a set time. Does not release any locks.
分享:
扫描分享到社交APP
上一篇
下一篇