杰瑞科技汇

Java wait/notify如何正确实现线程同步?

Of course! Let's dive deep into wait() and notify() in Java. These are fundamental but often tricky methods for achieving thread synchronization.

Java wait/notify如何正确实现线程同步?-图1
(图片来源网络,侵删)

The Core Problem: Why Do We Need wait() and notify()?

Imagine a classic producer-consumer scenario:

  • A Producer thread adds items to a shared buffer.
  • A Consumer thread removes items from the same shared buffer.

What if the buffer is empty? The consumer can't do anything. It shouldn't waste CPU cycles repeatedly checking if the buffer is full (this is called busy-waiting or "spinning"). It should be told to "wait" until an item is available.

What if the buffer is full? The producer can't add more items. It should wait until the consumer removes an item.

This is where wait() and notify() come in. They are the tools for inter-thread communication and efficient waiting.

Java wait/notify如何正确实现线程同步?-图2
(图片来源网络,侵删)

The Golden Rules: wait() and notify() Must Be Used Inside a Synchronized Block

This is the most important rule. You cannot call wait() or notify() on an object unless the current thread has the object's intrinsic lock.

Why?

  • wait(): Releasing the lock is the whole point. When a thread calls wait(), it:

    1. Temporarily releases the lock on the object.
    2. Puts itself into a "waiting" state (on the object's wait set).
    3. Waits for another thread to notify it. This allows other threads to enter the synchronized block and modify the object's state.
  • notify(): Waking up a thread requires holding the lock. When a thread calls notify(), it:

    Java wait/notify如何正确实现线程同步?-图3
    (图片来源网络,侵删)
    1. Wakes up one (arbitrarily chosen) thread from the object's wait set.
    2. The woken thread will not run immediately. It must re-acquire the lock before it can continue execution from the point where it called wait().

Method Breakdown

wait()

The wait() method tells the current thread to release the lock and wait until another thread calls notify() or notifyAll() on the same object.

There are three overloaded versions:

  • void wait(): Waits indefinitely until notified.
  • void wait(long timeout): Waits for a specified amount of time (in milliseconds). If not notified, it will wake up after the timeout.
  • void wait(long timeout, int nanos): Waits for a specified amount of time with nanosecond precision.

Key Behavior:

  • It must be called from within a synchronized block or method.
  • It releases the lock.
  • It can throw InterruptedException if the thread is interrupted while waiting. You should handle this.
  • When it "wakes up," it re-acquires the lock before proceeding. This means other threads might have changed the object's state while you were waiting. You must always re-check the condition you were waiting for.

notify()

The notify() method wakes up one single thread that is currently waiting on the same object.

Key Behavior:

  • It must be called from within a synchronized block or method.
  • It does not release the lock immediately. The thread that called notify() continues to hold the lock and will release it when it exits the synchronized block.
  • If multiple threads are waiting, which one gets woken up is not guaranteed (it's up to the JVM).

notifyAll()

The notifyAll() method wakes up all threads that are currently waiting on the same object.

Key Behavior:

  • It must be called from within a synchronized block or method.
  • All woken threads will try to re-acquire the lock. Only one will succeed, and the others will block again. This can lead to "thundering herd" problems, but it's often safer than notify() as it prevents threads from being missed.

The Producer-Consumer Example (Classic Implementation)

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

The Shared Resource (Buffer)

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

The Producer and Consumer Threads

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

The Main Class to Run Everything

public class Main {
    public static void main(String[] args) {
        SharedBuffer buffer = new SharedBuffer(2); // A small buffer to see waiting in action
        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: Simulation finished.");
    }
}

Why the while loop is CRITICAL (Spurious Wakeups)

You might see if (condition) { wait(); } in some old examples. This is a bug.

The Java specification allows for "spurious wakeups," where a thread can wake up from wait() for no apparent reason. If you use an if statement, the thread might wake up, find the condition is still true, and proceed incorrectly, violating the lock's invariant.

The correct pattern is always a while loop:

// WRONG
synchronized (lock) {
    if (!condition) {
        lock.wait(); // Might wake up spuriously and proceed incorrectly
    }
    // ... proceed
}
// CORRECT
synchronized (lock) {
    while (!condition) { // Re-check the condition after being woken up
        lock.wait();
    }
    // ... proceed
}

Modern Alternatives: java.util.concurrent

While wait() and notify() are foundational, modern Java provides higher-level concurrency utilities that are safer, easier to use, and less error-prone. You should prefer these for new code.

  1. BlockingQueue: This is the modern, go-to solution for the producer-consumer problem. It's a thread-safe queue that handles waiting and notifying internally.

    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    // Producer
    BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
    new Thread(() -> {
        try {
            for (int i = 0; i < 100; i++) {
                queue.put(i); // Blocks if the queue is full
                System.out.println("Produced " + i);
            }
        } catch (InterruptedException e) { /* ... */ }
    }).start();
    // Consumer
    new Thread(() -> {
        try {
            while (true) {
                Integer item = queue.take(); // Blocks if the queue is empty
                System.out.println("Consumed " + item);
            }
        } catch (InterruptedException e) { /* ... */ }
    }).start();
  2. java.util.concurrent.locks (Lock, Condition):

    • Lock is a more flexible and powerful replacement for synchronized.
    • Condition objects are created from a Lock and are the modern equivalent of wait()/notify(). They allow you to have multiple "wait conditions" for a single lock.
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    Lock lock = new ReentrantLock();
    Condition notFull = lock.newCondition();  // Condition for "buffer is not full"
    Condition notEmpty = lock.newCondition(); // Condition for "buffer is not empty"
    // In producer:
    lock.lock();
    try {
        while (isFull()) {
            notFull.await(); // Waits on the "notFull" condition
        }
        // ... produce item ...
        notEmpty.signal(); // Wakes up a consumer waiting on "notEmpty"
    } finally {
        lock.unlock();
    }

Summary: wait()/notify() vs. Modern Alternatives

Feature wait() / notify() BlockingQueue / Lock / Condition
Complexity Low-level, error-prone. High-level, safer.
Ease of Use Difficult to get right (spurious wakeups, missed signals). Much easier and more intuitive.
Flexibility One wait set per object. Condition objects allow multiple wait sets.
Best For Learning the fundamentals of concurrency. Almost all production applications.
Performance Can be very efficient when used correctly. Generally excellent, with well-tuned algorithms.
分享:
扫描分享到社交APP
上一篇
下一篇