杰瑞科技汇

Java中sleep和wait有何核心区别?

核心区别一览表

为了让你先有一个宏观的认识,我们用一个表格来总结它们最核心的区别:

Java中sleep和wait有何核心区别?-图1
(图片来源网络,侵删)
特性 sleep() (来自 Thread 类) wait() (来自 Object 类)
所属类 java.lang.Thread java.lang.Object
锁行为 不释放当前持有的锁 释放当前持有的锁
唤醒方式 自动唤醒(指定时间后) 必须被唤醒(需要另一个线程调用 notify()notifyAll()
使用位置 可以在任何地方调用 只能在 synchronized 代码块或方法中调用
用途 暂停线程执行,通常用于控制执行节奏轮询 线程间通信和协调,让线程进入等待状态,等待某个条件满足
异常 InterruptedException InterruptedExceptionIllegalMonitorStateException

详细解析

下面我们对每个方法进行深入剖析。

sleep() 方法

sleep()Thread 类中的一个静态方法。

基本用法:

try {
    // 让当前线程暂停 1000 毫秒(1秒)
    Thread.sleep(1000); 
} catch (InterruptedException e) {
    // 如果线程在睡眠期间被中断,会抛出 InterruptedException
    e.printStackTrace();
}

关键特性:

Java中sleep和wait有何核心区别?-图2
(图片来源网络,侵删)
  1. 不释放锁:这是 sleep()wait() 最根本的区别,当一个线程调用 sleep() 进入睡眠时,它依然持有它当前所拥有的任何锁,这意味着,即使它“睡着了”,其他需要这个锁的线程也无法进入相应的同步代码块。

    示例场景:

    public class SleepExample {
        private static final Object lock = new Object();
        public static void main(String[] args) {
            new Thread(() -> {
                synchronized (lock) {
                    System.out.println("线程 A 获取了锁,准备睡眠 2 秒...");
                    try {
                        Thread.sleep(2000); // 线程 A 睡觉,但依然持有锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程 A 睡眠结束,释放锁。");
                }
            }).start();
            new Thread(() -> {
                try {
                    // 等待一小会儿,确保线程 A 已经拿到锁并开始睡眠
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock) {
                    System.out.println("线程 B 尝试获取锁,但被线程 A 阻塞了!");
                }
            }).start();
        }
    }

    执行结果分析:

    • 线程 A 先获取锁,打印信息,然后调用 sleep(2000)
    • 线程 B 尝试获取锁,但由于线程 A 虽然在睡觉,但没有释放锁,所以线程 B 会被阻塞,无法进入 synchronized 代码块。
    • 2秒后,线程 A 醒来,打印信息,然后释放锁,线程 B 才能获得锁并执行。
  2. 自动唤醒sleep() 会指定一个睡眠时间,时间一到,线程会自动从 TIMED_WAITING 状态转换为 RUNNABLE 状态,等待 JVM 调度器再次分配 CPU 时间片。

    Java中sleep和wait有何核心区别?-图3
    (图片来源网络,侵删)
  3. 可中断:在睡眠期间,如果其他线程调用了该线程的 interrupt() 方法,sleep() 会立即抛出 InterruptedException,并中断睡眠,线程的中断状态会被清除。

主要用途:

  • 控制执行频率:在一个轮询任务中,每隔一段时间检查一次条件,避免 CPU 空转。
  • 模拟耗时操作:在测试或演示代码中模拟一些需要时间的操作。

wait() 方法

wait()Object 类中的一个实例方法,这意味着任何 Java 对象都可以调用 wait() 方法

基本用法:

synchronized (someObject) {
    try {
        // 让当前线程等待,直到被唤醒或被中断
        someObject.wait(); 
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

关键特性:

  1. 释放锁:当一个线程在 synchronized 代码块中调用 wait() 时,它会立即释放它所持有的 synchronized 代码块或方法对应的锁,该线程会进入 WAITING 状态,并等待在 someObject 的等待队列上。

  2. 必须被唤醒:调用 wait() 的线程不会自己醒来,它必须由另一个线程显式地调用同一个对象的 notify()notifyAll() 方法来唤醒。

    • notify():随机唤醒在该对象等待队列上的一个线程。
    • notifyAll():唤醒在该对象等待队列上的所有线程,这些线程会竞争锁,最终只有一个能获取到锁并继续执行。
  3. 只能在同步代码块中调用wait() 必须在 synchronized 上下文中调用,如果在非同步代码块中调用,会抛出 IllegalMonitorStateException,这是因为 wait()/notify() 机制是建立在锁的基础上的,线程必须先持有锁,才能释放锁并等待。

  4. 可中断:和 sleep() 一样,等待中的线程如果被 interrupt(),也会抛出 InterruptedException 并从等待状态中退出。

主要用途:

  • 线程间通信:这是 wait() 的核心用途,它使得一个线程可以等待某个条件成立,而另一个线程在条件满足后可以通知它。
  • 生产者-消费者模型:这是最经典的例子。
    • 消费者线程:检查共享缓冲区(一个对象)是否为空,如果为空,就调用 buffer.wait(),释放锁并等待。
    • 生产者线程:生产一个数据放入缓冲区,然后调用 buffer.notifyAll(),唤醒可能在等待的消费者线程。

示例场景:生产者-消费者

public class ProducerConsumerExample {
    private static final Object lock = new Object();
    private static int item = 0;
    public static void main(String[] args) {
        // 消费者线程
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("消费者:检查商品...");
                if (item == 0) {
                    System.out.println("消费者:没有商品,等待生产...");
                    try {
                        lock.wait(); // 释放锁,等待
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("消费者:商品已到货,开始消费!商品数量: " + item);
                item = 0;
            }
        }).start();
        // 生产者线程
        new Thread(() -> {
            try {
                // 确保消费者先启动并进入等待
                Thread.sleep(1000); 
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                System.out.println("生产者:正在生产商品...");
                item = 1;
                System.out.println("生产者:商品生产完毕,通知消费者!");
                lock.notify(); // 唤醒一个等待的线程
            }
        }).start();
    }
}

执行结果分析:

  1. 消费者线程启动,获取锁,发现 item 为 0,调用 lock.wait(),释放锁并进入等待状态。
  2. 生产者线程启动,1秒后获取锁,将 item 设为 1,然后调用 lock.notify()
  3. notify() 唤醒了消费者线程,消费者线程从 WAITING 状态变为 BLOCKED 状态,等待再次获取 lock
  4. 生产者线程执行完毕,释放 lock
  5. 消费者线程重新获取 lock,从 wait() 方法之后继续执行,打印消费信息。

总结与类比

为了更好地记忆,我们可以用一个生活中的类比来区分它们:

  • sleep() 就像你上床睡觉

    • 你在自己的床上睡(不释放你当前占据的资源/锁)。
    • 你定了个闹钟(指定了时间),闹钟响了(时间到自动醒来)。
    • 如果有人突然大声叫你(interrupt()),你也会被吵醒。
  • wait() 就像在餐厅等位

    • 你去餐厅发现没位置了,你对服务员说:“没位置的话,请叫我”(调用 wait(),释放餐桌这个“锁”)。
    • 你不能自己站起来说“有位置了”(不能自己醒来)。
    • 服务员看到有空位了,会喊你的号码(另一个线程调用 notify()),你被叫醒后去抢座位(重新获取锁)。
    • 服务员也可能喊“所有号码的顾客请过来”(notifyAll()),大家一起抢座位。

如何选择?

  • 如果你只是想让线程暂停一段时间,而不涉及与其他线程的锁交互,使用 Thread.sleep()
  • 如果你的线程需要等待某个条件,而这个条件由其他线程改变,并且在这个过程中需要释放锁以允许其他线程去改变这个条件,那么使用 Object.wait()notify()/notifyAll()

在现代 Java 并发编程中,更推荐使用 java.util.concurrent 包中的工具,如 LockCondition,它们提供了比 synchronizedwait()notify() 更强大和灵活的线程控制能力,但 wait()notify() 仍然是理解并发原理的基础。

分享:
扫描分享到社交APP
上一篇
下一篇