杰瑞科技汇

notify和wait在Java中如何正确使用?

Of course! Let's break down notify() and wait() in Java. This is a fundamental concept for multi-threading, used for communication and synchronization between threads.

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

The Core Idea: A Producer-Consumer Analogy

Imagine a single plate of food (a shared resource) and two people: a Chef (Producer thread) and a Customer (Consumer thread).

  • Chef's Job: Cook food and put it on the plate.
  • Customer's Job: Eat the food from the plate.

This leads to two problems:

  1. Race Condition: What if the Customer tries to eat from an empty plate? Or the Chef tries to add food to a full plate?
  2. Inefficiency: What if the Customer is waiting for food? Instead of continuously asking "Is the food ready yet?" (busy-waiting, which wastes CPU), the Customer should just go to sleep. When the Chef puts the food on the plate, they should wake the Customer up.

wait(), notify(), and notifyAll() are Java's tools to solve this exact problem.


The Three Methods

These methods are not part of the Thread class. They are part of the Object class. This is a crucial point. Any object in Java can be used as a lock (or monitor), and any object can have threads waiting on it.

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

wait()

  • What it does: Causes the current thread to pause execution and release the lock it holds on the object. The thread then goes into the "waiting" state.
  • Why it's important: It allows a thread to give up the CPU without busy-waiting, making the application much more efficient.
  • Key Rule: wait() can only be called from within a synchronized block or method. If you call it outside, you'll get an IllegalMonitorStateException.
  • Variants:
    • wait(): Waits indefinitely until another thread calls notify() or notifyAll() on this object.
    • wait(long timeout): Waits for a specified amount of time (in milliseconds). If it times out, it wakes up automatically.
    • wait(long timeout, int nanos): More precise version with nanoseconds.

notify()

  • What it does: Wakes up one single thread that is currently waiting on this object's monitor.
  • Which thread? If multiple threads are waiting, Java chooses one arbitrarily (though it's often not truly random, it's up to the JVM). You cannot specify which thread to wake up.
  • Important: The woken-up thread does not run immediately. It only moves from the "waiting" state to the "blocked" state, waiting to reacquire the lock. It will run only after the thread that called notify() releases the lock.
  • Key Rule: Like wait(), notify() can only be called from within a synchronized block or method.

notifyAll()

  • What it does: Wakes up all threads that are currently waiting on this object's monitor.
  • What happens next? Just like with notify(), all the woken-up threads move to the "blocked" state. They will then compete to reacquire the lock. Only one will succeed and continue; the others will go back to waiting.
  • When to use it: notifyAll() is generally safer than notify(). It prevents a situation where a thread is woken up but finds the condition it was waiting for is still not met (this is called a "signal-to-wasting" problem). notifyAll() is often used when any waiting thread could proceed.

The Golden Rules (The Contract)

The use of wait() and notify() is governed by a strict contract:

  1. Always use a while loop to check the condition, not an if statement.

    • Why? A thread can be woken up for reasons other than notify() (e.g., a spurious wakeup). When it wakes up, it must re-check the condition to see if it's actually safe to proceed. If you use an if, it might proceed even when the condition is false.
    • Correct Pattern:
      synchronized (lockObject) {
          while (!conditionIsMet) {
              lockObject.wait(); // Release lock and wait
          }
          // Now it's safe to proceed
      }
  2. Always call wait() and notify() from within a synchronized block or method.

    • Why? This ensures that only one thread can manipulate the shared state or the waiting queue at a time. It establishes the "happens-before" relationship, guaranteeing that changes made by one thread are visible to another.

Code Example: The Classic Producer-Consumer

Let's model our Chef and Customer scenario.

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

The Shared Resource (The Plate)

This object will hold the food and be used for locking.

// Plate.java
public class Plate {
    private String food;
    private boolean isEmpty = true; // Our condition variable
    // This is the "Consumer" action
    public synchronized void get() throws InterruptedException {
        // Use a while loop to check the condition
        while (isEmpty) {
            System.out.println("Customer: Plate is empty. Waiting...");
            // Release the lock and wait for a notification
            wait();
        }
        // If we get here, the plate is not empty
        System.out.println("Customer: Eating the " + food);
        this.food = null;
        this.isEmpty = true;
    }
    // This is the "Producer" action
    public synchronized void put(String food) throws InterruptedException {
        // Use a while loop to check the condition
        while (!isEmpty) {
            System.out.println("Chef: Plate is full. Waiting...");
            // Release the lock and wait
            wait();
        }
        // If we get here, the plate is empty
        System.out.println("Chef: Putting " + food + " on the plate.");
        this.food = food;
        this.isEmpty = false;
        // Notify the waiting customer that food is ready
        System.out.println("Chef: Notifying the customer.");
        notify(); // or notifyAll();
    }
}

The Threads (Chef and Customer)

// Chef.java
public class Chef extends Thread {
    private final Plate plate;
    public Chef(Plate plate) {
        this.plate = plate;
    }
    @Override
    public void run() {
        try {
            plate.put("Pizza");
            Thread.sleep(2000); // Simulate cooking time
            plate.put("Pasta");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
// Customer.java
public class Customer extends Thread {
    private final Plate plate;
    public Customer(Plate plate) {
        this.plate = plate;
    }
    @Override
    public void run() {
        try {
            plate.get();
            Thread.sleep(1000); // Simulate eating time
            plate.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

The Main Application

// ProducerConsumerDemo.java
public class ProducerConsumerDemo {
    public static void main(String[] args) {
        Plate plate = new Plate();
        Chef chef = new Chef(plate);
        Customer customer = new Customer(plate);
        customer.start();
        chef.start();
    }
}

Expected Output

The output will be something like this, showing the interaction:

Customer: Plate is empty. Waiting...
Chef: Putting Pizza on the plate.
Chef: Notifying the customer.
Customer: Eating the Pizza
Chef: Plate is full. Waiting...
Customer: Plate is empty. Waiting...
Chef: Putting Pasta on the plate.
Chef: Notifying the customer.
Customer: Eating the Pasta

Modern Alternatives

While wait() and notify() are foundational, modern Java provides higher-level concurrency utilities that are often safer and easier to use.

  1. java.util.concurrent.locks Package:

    • ReentrantLock: A more flexible and powerful alternative to synchronized. It has a Condition object, which is like a more advanced version of wait/notify. You can have multiple Condition objects for different states, and you can specify which thread to wake up.
    • Lock.lock() and Condition.await() / Condition.signal() are the modern equivalents.
  2. java.util.concurrent Package:

    • BlockingQueue (e.g., ArrayBlockingQueue, LinkedBlockingQueue): This is the recommended approach for most producer-consumer problems. It handles all the locking and waiting for you. You just put items into the queue and take them out. If the queue is full, a put() call will block. If it's empty, a take() call will block.

Example with BlockingQueue:

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ModernProducerConsumer {
    public static void main(String[] args) {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(1); // Plate with capacity for 1 item
        // Producer
        new Thread(() -> {
            try {
                System.out.println("Producer: Putting Pizza");
                queue.put("Pizza");
                Thread.sleep(2000);
                System.out.println("Producer: Putting Pasta");
                queue.put("Pasta");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        // Consumer
        new Thread(() -> {
            try {
                System.out.println("Consumer: Waiting for food...");
                String food1 = queue.take(); // Blocks if queue is empty
                System.out.println("Consumer: Eating " + food1);
                String food2 = queue.take(); // Blocks if queue is empty
                System.out.println("Consumer: Eating " + food2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Summary

Feature wait() / notify() Modern Alternatives (BlockingQueue, Lock)
Mechanism Low-level, manual control over threads and locks. High-level, abstracts away the complexity.
Safety Prone to errors if not used correctly (e.g., spurious wakeups). Much safer, handles edge cases internally.
Use Case Good for learning and simple, custom synchronization needs. Recommended for production code, especially producer-consumer patterns.
Complexity High. You manage the condition and the lock. Low. The framework manages the condition and the lock.
分享:
扫描分享到社交APP
上一篇
下一篇