join() 本质是基于线程对象监视器的 wait/notifyall 协作机制:调用时获取目标线程锁,检查存活状态,若运行则 wait 释放锁,jvm 在其终止时自动 notifyall 唤醒等待者。

因为 join() 方法本身没有轮询、不靠 sleep、也不依赖操作系统调度信号,它完全复用 Java 对象监视器(monitor)的原生协作机制——也就是以目标线程对象为锁,配合 wait() 挂起当前线程,并由 JVM 在目标线程终止时自动触发 notifyAll() 唤醒所有等待者。
join() 是 synchronized 方法,锁住的是目标线程对象
调用 t1.join() 时,当前线程(比如主线程)会尝试获取 t1 这个 Thread 实例对象的内部锁。这个锁不是任意对象,而是线程对象自身——也就是说,t1 就是锁粒度单位。只有拿到这把锁,才能进入 join 的同步块;而一旦进入,就立刻检查 t1.isAlive()。
- 若
t1还在运行,就执行t1.wait()—— 此时立刻释放 t1 对象锁,当前线程进入 WAITING 状态 - 若
t1已结束(TERMINATED),直接跳出,不 wait - 多个线程同时对同一个
t1调用 join,都会被阻塞在同一个对象锁上,也都会被同一次 notifyAll 唤醒
JVM 自动在目标线程终止时调用 notifyAll()
这不是开发者写的代码,也不是 Thread 类里显式调用的 notifyAll;而是 JVM 底层在该线程状态切换为 TERMINATED 的瞬间,自动在其对象上调用 notifyAll()。这是 Java 规范保证的行为,和普通对象不同:只有 Thread 实例才有这项“生命周期绑定唤醒”能力。
- 你不需要、也不能手动写
t1.notifyAll()—— 写了反而可能干扰逻辑或抛出 IllegalMonitorStateException - 即使没有其他线程显式 notify,只要
t1执行完 run(),等待中的线程就会被唤醒 - 这种设计避免了忙等待,也不消耗 CPU,是真正的“事件驱动型等待”
为什么非得用 wait(),不能用 sleep() 或 yield()?
关键不在“停顿”,而在状态判断与等待动作的原子性 + 锁释放 + 精准唤醒:
-
sleep()不释放锁 → 其他线程无法修改共享状态(比如无法安全设置 isAlive = false) -
yield()不挂起、不释放锁、不可控 → 完全无法保证等待效果 -
wait()必须在 synchronized 块中调用 → 保证isAlive()判断和后续 wait 是原子操作 -
wait()主动释放锁 → 允许目标线程正常完成并触发 JVM 的 notifyAll
带超时的 join(millis) 也是 wait 的增强版
join(long millis) 看似多了一个时间控制,但底层仍是基于 wait() 的循环重试逻辑:
- 每次 wait 返回后,重新计算剩余等待时间
- 如果发现
t1.isAlive() == false,立即退出,不等满时长 - 如果剩余时间 ≤ 0,也不再 wait,直接返回
- 整个过程仍围绕同一个对象锁展开,未脱离 wait/notifyAll 范式










