跳转至

Java 线程中的 wait 机制:深入理解与实践

简介

在 Java 多线程编程中,wait 机制是一个非常重要的工具,它允许线程之间进行有效的通信和协调。通过 wait,线程可以暂停执行,直到被其他线程唤醒。这在很多场景下都非常有用,比如生产者 - 消费者模型,线程需要等待某些条件满足后再继续执行。本文将深入探讨 wait 在 Java 线程中的基础概念、使用方法、常见实践以及最佳实践。

目录

  1. 基础概念
    • wait 方法的定义
    • 线程状态与 wait
  2. 使用方法
    • wait 方法的调用方式
    • notifynotifyAll 方法
  3. 常见实践
    • 生产者 - 消费者模型
    • 线程同步与资源共享
  4. 最佳实践
    • 避免死锁
    • 合理使用 waitnotify
  5. 小结
  6. 参考资料

基础概念

wait 方法的定义

wait 方法是 java.lang.Object 类的成员方法,这意味着 Java 中的任何对象都可以调用 wait 方法。它有三种重载形式: - void wait():导致当前线程等待,直到其他线程调用该对象的 notifynotifyAll 方法。 - void wait(long timeout):导致当前线程等待,直到其他线程调用该对象的 notifynotifyAll 方法,或者指定的毫秒数过去。 - void wait(long timeout, int nanos):与上一个方法类似,但可以指定更精确的等待时间,包括纳秒。

线程状态与 wait

当一个线程调用对象的 wait 方法时,它会释放对象的锁,并进入等待状态。在等待状态下,线程不会占用 CPU 资源,直到被其他线程唤醒。当其他线程调用该对象的 notifynotifyAll 方法时,等待的线程会被唤醒,重新竞争对象的锁,获取锁后继续执行。

使用方法

wait 方法的调用方式

wait 方法必须在同步代码块或同步方法中调用,否则会抛出 IllegalMonitorStateException 异常。下面是一个简单的示例:

public class WaitExample {
    public static void main(String[] args) {
        Object lock = new Object();
        Thread thread = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("线程开始等待...");
                    lock.wait();
                    System.out.println("线程被唤醒...");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();

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

        synchronized (lock) {
            System.out.println("主线程唤醒等待线程...");
            lock.notify();
        }
    }
}

在这个示例中,我们创建了一个线程,该线程在获取 lock 对象的锁后调用 wait 方法,进入等待状态。主线程休眠 2 秒后,获取 lock 对象的锁,并调用 notify 方法唤醒等待的线程。

notifynotifyAll 方法

notify 方法用于唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,那么只会唤醒其中一个线程,具体唤醒哪个线程是由 JVM 决定的。 notifyAll 方法用于唤醒在此对象监视器上等待的所有线程。所有被唤醒的线程会竞争对象的锁,获取锁的线程将继续执行。

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

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

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

        thread1.start();
        thread2.start();

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

        synchronized (lock) {
            System.out.println("主线程唤醒所有等待线程...");
            lock.notifyAll();
        }
    }
}

在这个示例中,我们创建了两个线程,它们都在等待 lock 对象的唤醒。主线程休眠 2 秒后,调用 notifyAll 方法唤醒所有等待的线程。

常见实践

生产者 - 消费者模型

生产者 - 消费者模型是 waitnotify 机制的经典应用场景。在这个模型中,生产者线程生成数据并将其放入共享缓冲区,消费者线程从共享缓冲区中取出数据进行处理。当缓冲区满时,生产者线程需要等待;当缓冲区空时,消费者线程需要等待。

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

public class ProducerConsumerExample {
    private static final int MAX_SIZE = 5;
    private static Queue<Integer> queue = new LinkedList<>();

    public static class Producer implements Runnable {
        @Override
        public void run() {
            for (int i = 1; i <= 10; i++) {
                synchronized (queue) {
                    while (queue.size() == MAX_SIZE) {
                        try {
                            System.out.println("缓冲区已满,生产者等待...");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    queue.add(i);
                    System.out.println("生产者生产: " + i);
                    queue.notify();
                }
            }
        }
    }

    public static class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.isEmpty()) {
                        try {
                            System.out.println("缓冲区为空,消费者等待...");
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    int item = queue.poll();
                    System.out.println("消费者消费: " + item);
                    queue.notify();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread producerThread = new Thread(new Producer());
        Thread consumerThread = new Thread(new Consumer());

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

在这个示例中,Producer 线程不断生成数据并放入 queue 中,当 queue 满时,调用 wait 方法等待;Consumer 线程不断从 queue 中取出数据,当 queue 为空时,调用 wait 方法等待。通过 notify 方法,生产者和消费者线程可以相互唤醒。

线程同步与资源共享

在多线程环境中,多个线程可能需要访问和修改共享资源。为了避免数据竞争和不一致性,我们可以使用 waitnotify 机制来实现线程同步。

public class ResourceSharingExample {
    private static int sharedResource = 0;

    public static class Incrementer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                synchronized (ResourceSharingExample.class) {
                    sharedResource++;
                    System.out.println("Incrementer: " + sharedResource);
                    ResourceSharingExample.class.notify();
                }
            }
        }
    }

    public static class Decrementer implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                synchronized (ResourceSharingExample.class) {
                    while (sharedResource == 0) {
                        try {
                            System.out.println("资源为 0,Decrementer 等待...");
                            ResourceSharingExample.class.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    sharedResource--;
                    System.out.println("Decrementer: " + sharedResource);
                    ResourceSharingExample.class.notify();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread incrementerThread = new Thread(new Incrementer());
        Thread decrementerThread = new Thread(new Decrementer());

        incrementerThread.start();
        decrementerThread.start();
    }
}

在这个示例中,Incrementer 线程和 Decrementer 线程共享 sharedResource 变量。Decrementer 线程在 sharedResource 为 0 时调用 wait 方法等待,Incrementer 线程增加 sharedResource 后调用 notify 方法唤醒 Decrementer 线程。

最佳实践

避免死锁

死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放锁时,就会发生死锁。为了避免死锁,我们可以采取以下措施: - 尽量减少锁的使用范围,只在必要的代码块中使用同步。 - 按照相同的顺序获取锁,避免不同线程以不同顺序获取锁。 - 使用 tryLock 方法来尝试获取锁,并设置合理的超时时间。

合理使用 waitnotify

  • 在调用 wait 方法前,确保已经获取了对象的锁。
  • wait 方法的条件判断中使用 while 循环,而不是 if 语句,以防止虚假唤醒。
  • 尽量使用 notifyAll 方法,除非你确定只需要唤醒一个线程。这样可以避免某些线程永远无法被唤醒的情况。

小结

wait 机制是 Java 多线程编程中实现线程通信和同步的重要工具。通过合理使用 waitnotifynotifyAll 方法,我们可以有效地协调多个线程的执行,解决诸如生产者 - 消费者模型、资源共享等实际问题。同时,遵循最佳实践可以帮助我们避免死锁等常见问题,提高多线程程序的稳定性和可靠性。

参考资料

希望通过本文的介绍,读者能够深入理解并高效使用 wait 在 Java 线程中的相关知识。如果有任何问题或建议,欢迎在评论区留言。