start 和 run、sleep 和 wait、join 和 yield 有什么区别?
简化版
线程的几个常用方法容易混:start() vs run()——start() 会启动一个新线程并让它执行 run 方法(真正的多线程);直接调 run() 只是在当前线程里执行 run 方法(普通方法调用,没有新线程)。sleep() vs wait()——sleep() 是 Thread 的静态方法、不释放锁、睡够时间自动醒;wait() 是 Object 的方法、释放锁、要在 synchronized 内、靠 notify 唤醒。join()——让当前线程等待另一个线程执行完再继续(t.join() = 等 t 结束)。yield()——让出 CPU(告诉调度器「我可以让一让」),但不保证真的让出,且不释放锁、线程仍是可运行状态。核心:start 启新线程/run 是普通调用、sleep 不释放锁/wait 释放锁、join 等别的线程结束、yield 让出 CPU(不保证)。
详细版
start() vs run():
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.start(); // 启动新线程,新线程执行 run → 输出 "Thread-0"(新线程名)
t.run(); // 只是在当前线程调用 run 方法 → 输出 "main"(当前线程名,没有新线程)
| 方法 | 效果 |
|---|---|
start() | 启动新线程,由新线程执行 run(真多线程),只能调一次 |
run() | 在当前线程里执行 run(普通方法调用,无新线程) |
sleep() vs wait():
| 维度 | Thread.sleep() | Object.wait() |
|---|---|---|
| 所属 | Thread(静态方法) | Object(实例方法) |
| 释放锁 | 不释放 | 释放 |
| 使用位置 | 任意位置 | 必须在 synchronized 内 |
| 唤醒方式 | 睡够时间自动醒(或被中断) | 靠 notify/notifyAll(或超时/中断) |
| 用途 | 单纯暂停一段时间 | 线程间等待-通知协作 |
join() 和 yield():
// join:让当前线程等待 t 执行完
Thread t = new Thread(task);
t.start();
t.join(); // 当前线程(如 main)阻塞,直到 t 执行完才继续
t.join(1000); // 最多等 1 秒
// yield:让出 CPU(提示调度器)
Thread.yield(); // 当前线程"让一让",但不保证真让出、不释放锁、仍是 RUNNABLE
⚠️ 最常考的坑是「直接调 run() 不会启动新线程」——很多人以为
t.run()能开启多线程,实际它只是「在当前线程里普通地调用 run 方法」(同步执行,没有新线程)。只有start()才会向 JVM 申请启动一个新线程、由新线程去执行 run。而且start()只能调用一次(一个线程对象只能启动一次,再调抛IllegalThreadStateException),run() 可以调多次(就是普通方法)。
完整版教学
一、start() vs run():启新线程 vs 普通调用
start() 和 run() 的区别是线程最基础也最常考的——start() 启动新线程、run() 只是普通方法调用:
start():
向 JVM 申请启动一个"新线程"
→ JVM 创建新的执行线程,让新线程去执行 run() 方法
→ 真正的多线程(run 在新线程里跑,和当前线程并发)
→ 只能调一次(线程只能启动一次)
run():
就是一个普通方法,直接调用它 = 在"当前线程"里执行 run 的代码
→ 没有启动任何新线程(同步执行,串行)
→ 可以调多次(普通方法)
关键区别:start() 是「启动新线程」(JVM 创建新线程执行 run),run() 是「普通方法调用」(在当前线程执行 run 代码,没有新线程)。所以「要多线程必须用 start()」——直接调 run() 只是「同步地跑了一遍 run 里的代码」,达不到并发效果。原理:start() 内部会调用 native 方法向操作系统申请创建线程,新线程就绪后由它回调 run()。理解「start 启新线程(真多线程、只能调一次)、run 是普通方法调用(在当前线程、无新线程、可多次)」,就掌握了这个最基础的区别——「要并发用 start 不用 run」。
二、sleep() vs wait():释不释放锁
sleep() 和 wait() 都能让线程「暂停」,但有本质区别,核心是「释不释放锁」(前面 wait/notify 题讲过,这里聚焦对比):
Thread.sleep(ms)(静态方法):
让"当前线程"睡眠指定时间,时间到自动醒
★ 不释放锁——如果在 synchronized 块里 sleep,仍持有锁(别的线程进不来)
用途:单纯"暂停一段时间"(如轮询间隔、限流等待)
Object.wait()(实例方法):
让当前线程在"某个对象上"等待,靠 notify/notifyAll 唤醒
★ 释放锁——wait 会释放它持有的那个对象的锁(好让别的线程能拿锁改条件、再 notify)
必须在 synchronized 块里调用(要先持有对象锁)
用途:线程间"等待-通知"协作(生产者消费者等)
最核心的区别:sleep 不释放锁(睡着还占着锁)、wait 释放锁(睡前先放锁)。为什么 wait 要释放锁?因为 wait 是「等某个条件满足」,需要别的线程「拿到锁 → 改变条件 → notify 唤醒它」——如果 wait 不释放锁,别的线程拿不到锁、改不了条件、也 notify 不了,就死锁了。而 sleep 只是「单纯睡一会」,不涉及等待条件,所以不需要释放锁。其他区别:sleep 是 Thread 静态方法(任意位置调)、wait 是 Object 方法(必须在 synchronized 内、靠 notify 唤醒)。理解「sleep 不释放锁(单纯暂停)、wait 释放锁(等待通知、必须 synchronized 内、靠 notify)」,就掌握了这对高频对比。
三、join():等待另一个线程结束
join() 用于「让当前线程等待另一个线程执行完」——t.join() 的意思是「等 t 结束我再继续」:
Thread t1 = new Thread(task1);
Thread t2 = new Thread(task2);
t1.start();
t2.start();
t1.join(); // main 线程阻塞,等 t1 执行完
t2.join(); // 再等 t2 执行完
// 到这里,t1 和 t2 都执行完了,main 继续
System.out.println("所有子任务完成");
join() 的用途是「等待子线程完成后再继续」——典型场景是「主线程启动多个子线程干活,然后 join 等它们都做完,再汇总结果」。t.join() 让「调用它的线程」(如 main)阻塞,直到 t 执行完毕才返回。t.join(timeout) 是带超时的(最多等指定时间)。原理:join 底层其实是用 wait() 实现的——调用 t.join() 的线程在 t 对象上 wait,t 执行完时会 notifyAll 唤醒等待它的线程。所以 join 也会释放锁(它内部是 wait)。注意 join 和前面的 CountDownLatch 类似(都能「等一组任务完成」),但 join 只能等「整个线程结束」、CountDownLatch 更灵活。理解「join 让当前线程等待目标线程结束、底层用 wait 实现、用于等子线程完成」,就掌握了 join 的用法。
四、yield():让出 CPU(提示)
yield() 用于「让出 CPU 时间片」——它告诉调度器「我可以让一让、让其他同优先级的线程先跑」:
Thread.yield(); // 当前线程让出 CPU(提示,不保证)
yield() 的特点:① 提示性(不保证)——它只是「建议」调度器「我愿意让出 CPU」,但调度器可以忽略(可能 yield 后当前线程又立即被调度);② 不释放锁——只是让出 CPU,持有的锁不释放;③ 线程状态不变——yield 后线程仍是 RUNNABLE(可运行)状态,不是阻塞(sleep/wait 会让线程进入等待/超时等待状态);④ 让给「同优先级或更高」的线程(不会让给更低优先级的)。yield 实际用得极少——因为它「不保证」(行为依赖 JVM/OS 调度),几乎无法用它实现可靠的控制。它偶尔用于「自旋等待时让出 CPU、减少空转」(如自旋锁里 yield 一下)。理解「yield 让出 CPU 但不保证(提示性)、不释放锁、线程仍 RUNNABLE、用得极少」,就掌握了 yield——它是「弱提示」,实际很少用。
五、几个方法对线程状态的影响
这几个方法对「线程状态」的影响不同,理解它们能加深对线程状态的认识(前面「线程状态」题讲过 6 种状态):
方法 线程进入的状态 是否释放锁
sleep(ms) TIMED_WAITING(超时等待) 不释放
wait() WAITING(等待) 释放
wait(ms) TIMED_WAITING(超时等待) 释放
join() WAITING(等目标线程) 释放(内部是 wait)
join(ms) TIMED_WAITING 释放
yield() RUNNABLE(仍可运行) 不释放
LockSupport.park() WAITING 不释放锁(但可被 unpark)
对比要点:① sleep/wait/join 会让线程进入「等待类状态」(WAITING 或 TIMED_WAITING)——线程真的「停下来」了;② yield 不改变状态(仍 RUNNABLE,只是让出当前时间片);③ 释放锁的只有 wait 和 join(内部是 wait),sleep 和 yield 不释放锁。这个对比把「暂停线程的几个方法」按「进入什么状态、释不释放锁」区分清楚了。理解「sleep/wait/join 进入等待状态、yield 仍 RUNNABLE、只有 wait/join 释放锁」,就把这些方法和线程状态串起来了。
六、实践中的选择
这些方法在实践中的选择:
要启动线程 → start()(绝不用 run(),run 不开新线程)
要暂停一段时间:
单纯等待固定时间(不涉及条件)→ Thread.sleep()(不释放锁,注意别在锁里 sleep 占着锁)
等待某个条件满足(需要别的线程通知)→ wait()/notify(释放锁)
但实际优先用更高层的工具:BlockingQueue、CountDownLatch、Condition(别手写 wait/notify)
要等其他线程完成 → join()(简单场景)或 CountDownLatch(更灵活)
要让出 CPU → yield()(几乎不用,不可靠;自旋等待可考虑 Thread.onSpinWait()(Java 9+,专为自旋优化))
实践准则:① 启动线程一定用 start();② 单纯暂停用 sleep(注意别在持锁时 sleep)、等待条件用高层工具(BlockingQueue/CountDownLatch/Condition)而非手写 wait/notify;③ 等线程完成用 join 或 CountDownLatch;④ yield 几乎不用(不可靠,自旋优化用 Java 9+ 的 Thread.onSpinWait())。核心是「用对方法、优先用高层并发工具」。理解「start 启线程、sleep 暂停、wait/notify 优先用高层工具替代、join/CountDownLatch 等完成、yield 少用」,就掌握了这些方法的实践选择。
记忆钩子:「start() 启新线程(真多线程、只能调一次)vs run() 普通方法调用(在当前线程、无新线程);sleep()(Thread 静态、不释放锁、睡够自动醒、任意位置)vs wait()(Object 方法、释放锁、synchronized 内、靠 notify);join() 让当前线程等目标线程结束(底层是 wait、释放锁);yield() 让出 CPU(提示不保证、不释放锁、仍 RUNNABLE、用得极少);释放锁的只有 wait 和 join」。
七、常见误区与追问
- 误区:调用 run() 就能启动多线程。 直接调 run() 只是在当前线程里普通地执行 run 方法(同步、无新线程);只有 start() 才启动新线程由它执行 run(真多线程)。
- 误区:start() 可以调用多次。 只能调一次——一个线程对象只能启动一次,再调 start() 抛 IllegalThreadStateException;run() 是普通方法可多次调。
- 误区:sleep 也会释放锁。 sleep 不释放锁——在 synchronized 里 sleep 仍持有锁(别的线程进不来);只有 wait 释放锁(睡前放锁让别的线程能改条件+notify)。
- 误区:yield 一定会让出 CPU。 yield 只是「提示」调度器可以让出,不保证真让出(可能 yield 后又立即被调度);且不释放锁、线程仍 RUNNABLE、实际用得极少。
- 追问:start() 和 run() 的区别? start() 向 JVM 申请启动新线程、由新线程执行 run(真多线程、只能调一次);直接调 run() 是普通方法调用、在当前线程执行 run 代码(无新线程、可多次调)。要并发必须用 start()。
- 追问:sleep() 和 wait() 的区别? sleep 是 Thread 静态方法、不释放锁、任意位置调、睡够自动醒(用于单纯暂停);wait 是 Object 方法、释放锁、必须在 synchronized 内、靠 notify 唤醒(用于线程间等待-通知协作)。
- 追问:join() 是怎么实现的? 底层用 wait 实现——调用 t.join() 的线程在 t 对象上 wait,t 执行完时 JVM 会 notifyAll 唤醒等待它的线程;所以 join 也会让当前线程进入 WAITING 状态、释放锁。
八、加强记忆
线程常用方法的对比:start() vs run()——start() 向 JVM 启动一个新线程、由新线程执行 run(真多线程、只能调一次否则抛 IllegalThreadStateException);直接调 run() 只是普通方法调用、在当前线程执行 run 代码(无新线程、可多次调)——要并发必须用 start()。sleep() vs wait()——sleep(Thread 静态方法、不释放锁、任意位置、睡够自动醒,用于单纯暂停);wait(Object 实例方法、释放锁、必须 synchronized 内、靠 notify 唤醒,用于等待-通知协作)——核心区别是释不释放锁(wait 释放是为了让别的线程改条件+notify)。join()——让当前线程等待目标线程执行完再继续(t.join() 等 t 结束,底层用 wait 实现、释放锁,用于等子线程完成,类似 CountDownLatch)。yield()——让出 CPU(提示不保证、不释放锁、线程仍 RUNNABLE、用得极少,自旋优化用 Java 9+ 的 Thread.onSpinWait())。状态影响:sleep→TIMED_WAITING、wait/join→WAITING、yield→仍 RUNNABLE;释放锁的只有 wait 和 join。一句话「start 启新线程只调一次/run 是普通调用无新线程、sleep 不释放锁/wait 释放锁靠 notify、join 等目标线程结束(内部 wait 释放锁)、yield 让 CPU 提示不保证不释放锁」。