导读:本期聚焦于河北彩花创作的《多线程会话管理中如何正确使用wait和notify实现线程同步?》,敬请观看详情。wait和notify是Java多线程开发中最容易被误用的两个方法。本文围绕多线程会话管理场景,系统讲解等待通知机制的工作原理,包括为什么wait必须放在synchronized代码块内调用、虚假唤醒如何用while循环规避、notify与notifyAll的选择策略等核心问题。文章还会结合一个会话池管理的完整实例,演示生产者消费者模型的实现细节,分析常见死锁成因以及Condition接口相比传统synchronized的优势,帮助读者写出线程安全且高效并发代码。

在Java多线程编程中,会话管理是一个典型的高并发场景:多个客户端线程不断创建、获取和释放会话资源,而会话本身数量有限,必须依靠同步机制协调各线程的行为。wait和notify正是JDK提供的最基础的线程间协作工具,但它们的使用规则相当严格,稍有疏忽就会引发死锁、虚假唤醒或非法监视器状态异常。本文将从原理到实践,完整剖析这套等待通知机制。

多线程会话管理中如何正确使用wait和notify实现线程同步?

一、wait/notify的底层工作原理

wait和notify定义在java.lang.Object类中,这意味着任何一个Java对象都可以作为同步锁和等待队列的载体。每个对象内部都关联着一个监视器(monitor),其中维护了两个关键结构:一个是入口队列,存放竞争锁的线程;另一个是等待队列,存放调用了wait方法的线程。

当线程在某个对象上调用wait()时,JVM会执行三个动作:第一,释放该对象持有的监视器锁;第二,把当前线程放入对象的等待集(wait set)并置为阻塞状态;第三,被唤醒后重新参与锁竞争,只有重新拿到锁才能从wait调用处返回。这个第三点非常关键,很多初学者以为notify之后线程立即恢复执行,实际上被唤醒的线程还需要重新排队获取锁。

notify()方法的作用是从等待集中随机挑选一个线程唤醒,而notifyAll()会唤醒等待集中的全部线程。被唤醒的线程并不会立刻运行,而是进入入口队列等待获取锁。正因如此,notifyAll之后往往只有拿到锁的那个线程能继续往下走,其余线程如果发现条件仍不满足,会再次调用wait进入等待状态,形成经典的条件轮询模式。

还有一个重要的规则:wait和notify必须在持有对应对象监视器锁的前提下调用,否则会抛出IllegalMonitorStateException。看一个错误示例:

public class WrongDemo {
    private final Object lock = new Object();

    public void consumer() {
        // 错误:没有持有lock的监视器锁就直接调用wait
        lock.wait(); // 抛出 IllegalMonitorStateException
    }
}

正确做法是先通过synchronized获取锁,再在同步块内调用wait。这背后的设计原因是:检查共享条件本身就需要锁保护,否则会在检查条件与进入等待之间产生竞态窗口,导致通知丢失。

二、多线程会话管理的完整实战

下面用一个会话池的例子来演示等待通知机制的典型应用。假设系统维护一个固定容量的会话队列,处理线程从队列中取会话进行业务处理,网络线程不断把新会话放入队列。当队列空时处理线程等待,队列满时网络线程等待,这本质上就是生产者消费者模型。

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

public class SessionPool {
    private final Queue<String> sessions = new LinkedList<>();
    private final int capacity;
    private final Object notEmpty = new Object();

    public SessionPool(int capacity) {
        this.capacity = capacity;
    }

    // 生产者:网络线程放入新会话
    public void offer(String sessionId) throws InterruptedException {
        synchronized (notEmpty) {
            // 必须用while而不是if,防止虚假唤醒
            while (sessions.size() == capacity) {
                notEmpty.wait(); // 队列已满,等待消费者释放空间
            }
            sessions.offer(sessionId);
            notEmpty.notifyAll(); // 唤醒等待的消费者
        }
    }

    // 消费者:工作线程取出会话处理
    public String take() throws InterruptedException {
        synchronized (notEmpty) {
            while (sessions.isEmpty()) {
                notEmpty.wait(); // 队列为空,等待生产者投放
            }
            String session = sessions.poll();
            notEmpty.notifyAll(); // 唤醒等待的生产者
            return session;
        }
    }
}

上面代码中有两个细节值得反复强调。第一个是while循环的使用。JDK官方文档明确指出,线程可能在没有被notify的情况下“虚假唤醒”(spurious wakeup),也可能在notify发出后、重新拿到锁之前条件又被其他线程改变了。因此醒来后必须重新检查条件,用if判断一次就继续执行的做法存在严重隐患。

第二个细节是notifyAll与notify的选择。notify只随机唤醒一个线程,如果等待队列中既有生产者又有消费者,notify可能唤醒的是同类型线程,导致真正需要执行的线程继续沉睡,这就是所谓的信号丢失问题。notifyAll虽然会带来一定的锁竞争开销,但在正确性上更稳妥。一般原则是:除非能确定所有等待线程都在等同一个条件,否则优先使用notifyAll。

另外注意wait()wait(long timeout)的区别。无参wait会无限期等待,而带超时参数的版本在等待指定毫秒后自行返回,适合实现会话超时自动回收这类功能。Thread.interrupted会抛出InterruptedException,因此生产环境中不要吞掉这个异常,应该捕获后恢复中断标记或者向上传播,让上层框架有机会感知线程的终止请求。

三、常见陷阱与Condition替代方案

使用wait/notify时最常见的问题有三个。一是锁对象不一致:在一个对象上加锁,却在另一个对象上调用wait,两个线程看似同步实则毫无关联。二是唤醒顺序错误:先释放锁再notify的代码顺序虽然不会编译报错,但如果notify发生在消费者进入等待之前,通知就永远丢失了,消费者将陷入无限等待。三是死锁:多个线程以不同顺序获取多把锁,或线程持有锁的同时等待自己释放锁的条件。

排查这类问题可以使用jstack工具导出线程快照,观察BLOCKED和WAITING状态的线程及其持有的锁信息。在代码层面,尽量将锁、条件判断和状态修改收敛到同一个类中,减少跨对象协作。

从JDK 5开始,java.util.concurrent.locks包提供了Condition接口作为wait/notify的现代化替代品。它的核心优势在于一个锁可以挂多个等待队列,而synchronized对象的等待集只有一个。对于会话池这种既有“非空”又有“非满”两个条件的场景,两个Condition能让通知精确命中目标线程,避免无意义的批量唤醒。

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionSessionPool {
    private final Queue<String> sessions = new LinkedList<>();
    private final int capacity;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    public void offer(String sessionId) throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (sessions.size() == capacity) {
                notFull.await();      // 只等待非满条件
            }
            sessions.offer(sessionId);
            notEmpty.signal();        // 只唤醒等待非空条件的线程
        } finally {
            lock.unlock();
        }
    }

    public String take() throws InterruptedException {
        lock.lockInterruptibly();
        try {
            while (sessions.isEmpty()) {
                notEmpty.await();
            }
            String session = sessions.poll();
            notFull.signal();
            return session;
        } finally {
            lock.unlock();
        }
    }
}

Condition的await对应wait,signal对应notify,signalAll对应notifyAll,语义几乎一致但功能更丰富,例如支持超时到纳秒精度的awaitNanos、可不响应中断的awaitUninterruptibly等。更重要的是,ReentrantLock还支持公平锁模式,让等待最久的线程优先获得锁,这对会话处理顺序敏感的业务很有价值。

实际工程中,如果不需要如此精细的控制,直接使用现成的LinkedBlockingQueue就是最佳选择,它内部正是基于两把锁和两个Condition实现的高性能阻塞队列。理解wait/notify的意义在于掌握底层协作原理,无论日后使用线程池、消息队列还是分布式锁,这套条件等待的思维方式都是通用的。

总结一下要点:wait必须配合synchronized使用并在while循环中检查条件;优先考虑notifyAll保证唤醒的正确性;条件复杂时用Condition拆分等待队列;始终正确处理InterruptedException。掌握这些规则,多线程会话管理的同步问题就能迎刃而解。

多线程会话管理wait notify线程同步修改时间:2026-09-01 20:22:40

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。