前言:

  1. sleep(1000) 是 Thread 中的方法,参数是sleep多少毫秒。sleep() 的作用是将当前线程暂停一段时间,但这期间不会释放锁
  2. wait、notify、notifyAll 是 Object 中的方法,可以作用于任何对象,用于控制线程的状态,通常配合 synchronized 代码块使用

一、先分清核心定位:

sleep 是 “休眠”,wait/notify/notifyAll 是 “线程通信”

  • sleep:属于 Thread 类的静态方法,核心是让线程 “休眠” 指定时间,是线程自身的状态控制
  • wait/notify/notifyAll:属于 Object 类的方法(所有对象都有),核心是实现线程间的通信与协作(比如生产者 - 消费者模型),依赖对象的内置锁(Monitor)。

二、sleep 详解(线程休眠,不释放锁)

通俗理解:就像你在办公室里锁着门加班,定了 10 分钟闹钟后趴在桌上休息 —— 门还锁着(持有锁),闹钟响了(时间到)才起来继续干活。

专业核心点

  1. 归属Thread.sleep(long millis),静态方法,只能让当前执行线程休眠;

  2. 锁行为:休眠期间不会释放持有的任何锁(比如 synchronized 锁);

  3. 异常处理:必须捕获 / 声明 InterruptedException(休眠时线程可能被中断);

  4. 状态变化:线程从「运行态」→「阻塞态」,时间到后回到「就绪态」等待 CPU 调度;

  5. 使用场景:模拟延迟(比如接口重试间隔)、降低线程执行频率(定时轮询)。

  6. sleep 的作用是使当前线程睡眠指定的时间,放弃对CPU的占用,但是不会放弃对线程以获取到的锁,例:

public class SleepTest extends Thread{

    // 锁
    public static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        SleepTest sleepTest = new SleepTest();
        sleepTest.setName("test1");
        sleepTest.start();

        SleepTest sleepTest2 = new SleepTest();
        sleepTest2.setName("test2");
        sleepTest2.start();

    }

    @Override
    public void run() {
        System.out.println("ThreadName :" + Thread.currentThread().getName() + " start" );
        synchronized (lock){
            for (int i =0 ; i<5; i++){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("ThreadName :" + Thread.currentThread().getName() + "; i == " + i);
            }
        }
    }
}

打印结果:

ThreadName :test2 start
ThreadName :test1 start
ThreadName :test2; i == 0
ThreadName :test2; i == 1
ThreadName :test2; i == 2
ThreadName :test2; i == 3
ThreadName :test2; i == 4
ThreadName :test1; i == 0
ThreadName :test1; i == 1
ThreadName :test1; i == 2
ThreadName :test1; i == 3
ThreadName :test1; i == 4

在main方法中我们,我们创建了两个线程,它们共同竞争同一把锁 lock ,根据打印结果可以看出,线程1 一直等到线程2 执行结束之后才执行 synchronized 中代码块, sleep(1000) 方法在线程2 中并没有释放 锁lock ,直至执行结束

三、等待池、锁池简介

了解这个wait、notify、notifyAll三个方法之前,需要先了解两个概念:等待池、锁池;

  1. 等待池:当对象锁【lock】 调用 wait 方法时, 持有该对象锁的线程会进入到等待池中,并且释放当前对象锁,线程进入到被动状态,需要notify notifyAll 将其唤醒。
  2. 锁池: 假设某对象锁当前某线程获取,其他线程想要持有该对象锁时,需要进入到锁池中,等待对象锁被释放再去竞争锁,线程进入到主动状态,等待对象锁空闲。

四、wait 详解(线程等待,释放锁)

通俗理解:你在办公室锁着门干活,突然发现缺材料,于是把门锁打开(释放锁),走到茶水间等同事送材料(进入等待队列)—— 直到同事喊你(notify),你再回去抢门锁(重新竞争锁)继续干活。

专业核心点

  1. 归属Object.wait()/wait(long timeout),实例方法,必须在 synchronized 同步块 / 方法中调用(否则抛 IllegalMonitorStateException);
  2. 锁行为:调用瞬间立刻释放持有的对象锁(这是和 sleep 最核心的区别);
  3. 状态变化:线程从「运行态」→「等待态」,进入该对象的 Monitor 等待队列;
  4. 唤醒条件
    • 其他线程调用该对象的 notify()/notifyAll()
    • 等待超时(调用 wait(long) 时);
    • 线程被中断(调用 interrupt());
  1. 唤醒后行为:线程不会立刻执行,需重新竞争对象锁,拿到锁后才会从 wait() 处继续执行;
  2. 使用场景:线程间依赖等待(比如生产者没生产,消费者等待)。
synchronized (lock) {
    while (条件不满足) { // 用while防止虚假唤醒,面试加分点
        lock.wait(); // 释放lock锁,进入等待队列
    }
    // 条件满足,执行业务逻辑
}

五、notify/notifyAll 详解(唤醒等待线程)

通俗理解:notify 是 “喊一个人”(随机唤醒等待队列中的一个线程),notifyAll 是 “喊所有人”(唤醒等待队列中的所有线程)—— 被喊的人需要重新抢门锁(竞争锁)才能干活。

1. notify()

专业核心点

  • 归属:Object.notify(),必须在 synchronized 同步块 / 方法中调用;
  • 作用:随机唤醒该对象 Monitor 等待队列中的一个线程,被唤醒的线程进入「就绪态」,需重新竞争锁;
  • 注意:调用后当前线程不会立刻释放锁,需执行完同步代码块 / 方法才释放。

2. notifyAll()

专业核心点

  • 作用:唤醒该对象 Monitor 等待队列中的所有线程,所有线程进入「就绪态」竞争锁,最终只有一个线程能拿到锁执行;
  • 对比 notify ():notify () 可能导致 “虚假唤醒”(比如多个消费者等待,只唤醒一个但条件仍不满足),生产环境优先用 notifyAll ();
  • 使用场景:多线程等待同一条件(比如多个消费者等待生产者生产)。

代码示例(生产者唤醒消费者)

synchronized (lock) {
    // 生产数据,满足条件
    lock.notifyAll(); // 唤醒所有等待的消费者线程
}
  1. wait、notify、notifyAll 需要配合 synchronized (lock) 来使用,因为对对象锁的进行操作之前需要,对其先进行监听操作。
  2. wait() : 会将当前线程进入休眠状态,并且释放当前获取到的对象锁,并将当前线程放入到等待池中,等待notify、notifyAll 将其唤醒并进入锁池中竞争对象锁,
  3. notify 从等待池中随机选择【和优先级也有关】唤醒其中一个线程,进入到锁池中等待对象锁,并执行,其他等待池中线程继续等待。
  4. notifyAll 将等待池中所有的线程全部唤醒到锁池中,然后去竞争对象锁,
    例 :

六、测试

四种综合

public class WaitTest extends Thread{

    // 锁
    public static final Object lock = new Object();

    public static final Object lock1 = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 创建线程一
        WaitTest sleepTest = new WaitTest();
        sleepTest.setName("test1");
        sleepTest.start();

        // 创建线程二
        WaitTest sleepTest2 = new WaitTest();
        sleepTest2.setName("test2");
        sleepTest2.start();

        // sleep 保证线程一、二调用 wait 进入到等待池中
        Thread.sleep(1000);
        synchronized (lock){
            // 随机唤醒一个线程进入到锁池中等待对象锁
            System.out.println("lock.notify");
            lock.notify();
        }

    }

    @Override
    public void run() {
        synchronized (lock){
            try {
                System.out.println("Thread-Name :" + Thread.currentThread().getName() + " wait ");
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int i =0 ; i< 5; i++){
                System.out.println("Thread-Name :" + Thread.currentThread().getName() + "; i == " + i);
            }
        }
    }
}

打印结果:

Thread-Name :test2 wait 
Thread-Name :test1 wait 
lock.notify
Thread-Name :test2; i == 0
Thread-Name :test2; i == 1
Thread-Name :test2; i == 2
Thread-Name :test2; i == 3
Thread-Name :test2; i == 4

主线程sleep(1000) 期间,线程一二都通过 wait 进入到等待池中,在sleep() 之后,调用lock.notify(),将线程二唤醒进入到锁池中,线程一继续等待被唤醒。

  1. lock.notify(); 换成 lock.notifyAll(); 之后打印结果:
Thread-Name :test1 wait 
Thread-Name :test2 wait 
lock.notifyAll
Thread-Name :test2; i == 0
Thread-Name :test2; i == 1
Thread-Name :test2; i == 2
Thread-Name :test2; i == 3
Thread-Name :test2; i == 4
Thread-Name :test1; i == 0
Thread-Name :test1; i == 1
Thread-Name :test1; i == 2
Thread-Name :test1; i == 3
Thread-Name :test1; i == 4

根据结果可以看出,线程一二全部被唤醒,等待 lock 锁之后全部执行完毕。

生产者消费模型

通俗解释:生产者和消费者共用一个 “货架(队列)”,货架满了生产者就 wait(释放锁),等消费者拿走产品后唤醒;货架空了消费者就 wait(释放锁),等生产者放产品后唤醒,实现线程间的协调。

生产者消费者模型是 Java 并发编程中最经典的线程通信场景,核心目的是协调 “生产资源” 和 “消费资源” 的线程,避免生产过剩或消费空资源

我用大白话解释:

  1. 核心角色
    • 生产者线程:负责 “生产” 数据 / 资源(比如往队列里放产品);
    • 消费者线程:负责 “消费” 数据 / 资源(比如从队列里取产品);
    • 缓冲容器:通常是队列 / 数组(相当于 “货架”),用来存生产的资源,解耦生产者和消费者的速度差异。
  1. 核心问题
    • 生产者不能往满的缓冲容器里生产(否则溢出);
    • 消费者不能从空的缓冲容器里消费(否则没东西可拿)。
  1. 解决方式:用wait()notify()/notifyAll()实现线程间通信:
    • 缓冲容器满时,生产者调用wait()释放锁,进入等待状态;
    • 消费者消费后,调用notify()唤醒生产者继续生产;
    • 缓冲容器空时,消费者调用wait()释放锁,进入等待状态;
    • 生产者生产后,调用notify()唤醒消费者继续消费。

简单总结:生产者和消费者通过缓冲容器隔离,靠wait()/notify()协调节奏,避免 “生产到满还在生产”“消费到空还在消费” 的问题。

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

// 生产者消费者模型
public class ProducerConsumer {
    // 产品队列(最多存5个)
    private static final Queue<String> QUEUE = new LinkedList<>();
    private static final int MAX_SIZE = 5;
    private static final Object LOCK = new Object();

    // 生产者:生产产品
    static class Producer extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (LOCK) {
                    // 队列满了,等待消费者消费
                    while (QUEUE.size() == MAX_SIZE) {
                        try {
                            System.out.println("生产者:队列满了,等待消费");
                            LOCK.wait(); // 释放锁,等消费者唤醒
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 生产产品
                    String product = "产品" + System.currentTimeMillis();
                    QUEUE.add(product);
                    System.out.println("生产者:生产了" + product + ",队列长度:" + QUEUE.size());
                    LOCK.notifyAll(); // 唤醒消费者
                }
                // 模拟生产耗时
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 消费者:消费产品
    static class Consumer extends Thread {
        @Override
        public void run() {
            while (true) {
                synchronized (LOCK) {
                    // 队列空了,等待生产者生产
                    while (QUEUE.isEmpty()) {
                        try {
                            System.out.println("消费者:队列空了,等待生产");
                            LOCK.wait(); // 释放锁,等生产者唤醒
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 消费产品
                    String product = QUEUE.poll();
                    System.out.println("消费者:消费了" + product + ",队列长度:" + QUEUE.size());
                    LOCK.notifyAll(); // 唤醒生产者
                }
                // 模拟消费耗时
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        new Producer().start();
        new Consumer().start();
    }
}

img

七、总结

维度 sleep wait
所属类 Thread(静态) Object(实例)
锁释放 不释放 立刻释放
调用位置 任意位置 必须在 synchronized 同步块 / 方法
唤醒方式 时间到自动唤醒 需 notify/notifyAll/ 超时 / 中断
异常处理 需处理 InterruptedException 需处理 InterruptedException
维度 notify() notifyAll()
唤醒数量 随机唤醒 1 个等待线程 唤醒所有等待线程
竞争成本 低(仅一个线程竞争锁) 高(所有线程竞争锁)
风险 易出现虚假唤醒 无虚假唤醒风险
适用场景 单生产者 - 单消费者 多生产者 - 多消费者(生产环境首选)
  • wait 必须用 while 循环判断条件:不能用 if—— 因为线程被 notify () 唤醒后,条件可能已经被其他线程修改(虚假唤醒),while 会重新检查条件,if 只会检查一次;
  • notify () 后不会立刻释放锁:比如线程 A 调用 lock.notify() 后,需执行完同步代码块,才会释放 lock 锁,线程 B(被唤醒的)才能竞争锁;
  • sleep 不释放锁的坑:若在同步块中调用 sleep,其他线程会一直阻塞,直到 sleep 结束、当前线程释放锁,这是新手常犯的性能问题;
  • wait/notify 必须对应同一个对象:线程 A 调用 obj1.wait(),必须由其他线程调用 obj1.notify() 才能唤醒,调用 obj2.notify() 无效。

整体总结

sleep 是线程自身的休眠控制,不释放锁,适合简单的延迟场景;wait/notify/notifyAll 是线程间通信的核心,依赖对象锁,wait 释放锁、notify/notifyAll 唤醒等待线程,是实现生产者 - 消费者等协作模型的基础。

核心记住三个关键点:

  1. sleep 归 Thread,wait/notify 归 Object;
  2. sleep 不释放锁,wait 释放锁;
  3. wait/notify 必须在 synchronized 中调用,生产环境优先用 notifyAll () + while 循环防虚假唤醒。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐