Java 线程从“萌新”到“躺平”:生命周期和调度机制真的这么玄乎?
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
说到多线程,大家大概率都经历过这么个阶段:
“线程?不就
new Thread().start()嘛,有啥好说的?”
然后过一阵子,线上一波线程泄露、CPU 飙满、死锁、奇怪卡顿,你开始怀疑人生:
“这些线程到底在干嘛?为什么有的在跑、有的在等、有的一直不结束?”
这时候,八成就是线程生命周期和线程调度机制没搞明白。
今天我们就把这俩东西好好捋一遍:
- 线程从出生到“退役”的完整生命周期到底长啥样?
- Java 里的
NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED到底怎么来的? - 操作系统和 JVM 是怎么“排队安排线程上 CPU”的?
sleep()、wait()、join()、yield()都在调度里扮演什么角色?- 实战中有哪些坑,能提前避一避?
保证看完不会立刻变成“并发大佬”,但至少:
以后问你“线程有哪些状态、是怎么切换的”,你不会再只会说一句“就…Running 和 Dead 吧?”😅
一、线程是啥,先别急着上来就 start() 🚶♂️
简单粗暴地说:
- 进程(Process):一个程序的“整体运行实例”,有自己独立的内存空间。
- 线程(Thread):进程里的“执行单元”,共享进程的内存资源,可以同时干活。
对 Java 而言:
- 一个 JVM 进程里可以有很多线程:GC 线程、应用线程、后台线程等等。
- 每个
new Thread()出来的家伙,底层都会映射成一个 操作系统级别的线程(HotSpot 经典实现)。 - 线程调度由 操作系统 + JVM 一起配合 完成。
所以你可以把“线程生命周期”和“调度机制”理解为:
JVM 和 OS 一起养了一群线程,从出生、分配 CPU,到休眠、阻塞,最后收尸(回收)的一整套流程。
二、Java 线程生命周期:从出生到“终身退休”的那几步
Java 线程有一套比较“官方”的状态划分,Thread.State 枚举里写得很清楚:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
别看只有六个状态,组合起来能演四大文明史。我们一个一个拆开聊。
0. 先来一张“精神地图”🧠
用一张简化版“线程人生轨迹图”感受一下(ASCII 版凑合看下):
NEW
|
v
start()
|
v
RUNNABLE <-------------------------------+
/ | \ |
/ | \ |
v v v |
BLOCKED WAITING TIMED_WAITING |
^ ^ ^ |
| | | |
| | +-- sleep()/wait(timeout)/join(timeout)
| +-- wait()/join()/park() |
+-- 获得锁 |
|
run() 方法执行完 / 抛异常 |
v |
TERMINATED ---------+
下面我们用“故事模式”来解释每个状态。
1. NEW:刚出生但还没开始干活 👶
Thread t = new Thread(() -> System.out.println("hello"));
System.out.println(t.getState()); // NEW
特点:
- 线程对象已经创建,但是还没调用
start()。 - 这时候线程还没被操作系统“登记”,更别提调度上 CPU 了。
注意:调用 run() 不会改变状态为 RUNNABLE,这只是一个普通方法调用:
Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run(); // 这行只是当前线程(比如 main)执行 run 方法
只有调用 start(),才会:
- 请求 JVM 建立一个新的操作系统线程
- 让它进入调度队列
2. RUNNABLE:在跑,或者等着跑 🏃♂️
在 Java 里,RUNNABLE 状态有点“综合症”性质:
表示线程要么正在运行,要么正在就绪队列里排队,等待被操作系统调度执行。
Thread t = new Thread(() -> {
while (true) {
// do sth
}
});
t.start();
System.out.println(t.getState()); // 很可能是 RUNNABLE
特点:
- RUNNABLE ≈ OS 级别的 Ready + Running 状态的统称。
- 线程可能真的在 CPU 上跑,也可能刚被抢占、等下一个时间片。
线程什么时候会变成 RUNNABLE?
- 从
NEW调用start()之后 - 从
BLOCKED拿到锁之后 - 从
WAITING被唤醒后 - 从
TIMED_WAITING超时 / 被唤醒后
3. BLOCKED:等锁等到怀疑人生 🔒
当线程想进入某个 synchronized 受保护的代码块 / 方法,但锁被别人占着,就会变成 BLOCKED。
public class BlockedDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
try {
Thread.sleep(5000); // 占用锁 5 秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK) {
System.out.println("t2 got lock");
}
});
t1.start();
Thread.sleep(100); // 确保 t1 先拿到锁
t2.start();
Thread.sleep(1000);
System.out.println("t2 state = " + t2.getState()); // 大概率是 BLOCKED
}
}
特点:
BLOCKED只针对 synchronized 的 monitor 锁。- 是在等“锁的进入许可”,而不是因为
wait()、sleep()之类。 - 一旦拿到锁,就会回到 RUNNABLE 状态。
简单说:
- 想进 synchronized,结果有人在里面蹲着不出来 → 你就 BLOCKED。
4. WAITING:没人叫你,你就一直等着 🧘♂️
WAITING 是“无限期等待”,常见原因有:
Object.wait()(不带超时)Thread.join()(不带超时)LockSupport.park()
来看个 wait/notify 的例子:
public class WaitingDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) throws Exception {
Thread waiter = new Thread(() -> {
synchronized (LOCK) {
try {
System.out.println("waiter: going to wait...");
LOCK.wait(); // 进入 WAITING
System.out.println("waiter: awakened");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
waiter.start();
Thread.sleep(500); // 确保 waiter 先 wait
System.out.println("waiter state = " + waiter.getState()); // WAITING
Thread notifier = new Thread(() -> {
synchronized (LOCK) {
System.out.println("notifier: notify one");
LOCK.notify();
}
});
notifier.start();
}
}
特点:
- 线程主动说:“我等着,什么时候有人叫我(notify/join 完成/unpark),我再继续。”
- 没超时时间,理论上你不叫,它就一直等,直到线程被中断或者 notify。
5. TIMED_WAITING:我等你一会,但别超过时间 ⏳
TIMED_WAITING 是“带时间限制的等待”,常见来源:
Thread.sleep(millis)Object.wait(timeout)Thread.join(timeout)LockSupport.parkNanos()/parkUntil()
例子:
public class TimedWaitingDemo {
public static void main(String[] args) throws Exception {
Thread sleeper = new Thread(() -> {
try {
Thread.sleep(3000); // TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
});
sleeper.start();
Thread.sleep(500);
System.out.println("sleeper state = " + sleeper.getState()); // TIMED_WAITING
}
}
特点:
- 线程在“睡觉”或者“限时等某件事发生”,超时后自动回到 RUNNABLE 队列。
- 多用于:超时等待锁、超时等待 IO、定时任务等场景。
6. TERMINATED:线程人生走完了 💀
当线程的 run() 方法执行结束,或者抛出未捕获异常,线程就进入 TERMINATED 状态。
public class TerminatedDemo {
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> System.out.println("do something"));
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // 基本就是 TERMINATED 了
}
}
特点:
- 线程结束后,就不能再启动了,调用
start()会直接抛异常:
t.start();
t.start(); // 会抛 IllegalThreadStateException
一个线程对象的生命周期,只能经历一次:
NEW → … → TERMINATED,不能重来。
三、来一段完整 demo,看状态如何真实变换 🎬
我们写一段小代码,一次性把几个状态串起来看:
public class ThreadLifeCycleDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {
System.out.println("1. 子线程开始,当前状态:" + Thread.currentThread().getState());
// sleep -> TIMED_WAITING
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK) {
try {
// wait -> WAITING
LOCK.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("2. 子线程被唤醒,准备结束");
}, "demo-thread");
System.out.println("main: t state = " + t.getState()); // NEW
t.start();
Thread.sleep(100); // 确保子线程进入 sleep
System.out.println("main: t state after start = " + t.getState()); // RUNNABLE or TIMED_WAITING
Thread.sleep(600); // 此时 sleep 已结束,进入 wait
System.out.println("main: t state after wait = " + t.getState()); // WAITING
synchronized (LOCK) {
LOCK.notify(); // 唤醒子线程
}
Thread.sleep(100);
System.out.println("main: t final state = " + t.getState()); // TERMINATED
}
}
当然,每次输出的状态可能会略有差异(调度是动态的),
但大体能看到一条“从 NEW 出生,一路折腾到 TERMINATED”的主线。
四、线程调度机制:谁能上 CPU,不是你说了算 🧮
线程状态搞清楚之后,另一个问题自然来了:
“这么多线程,CPU 就几个核,谁先跑?跑多久?怎么换?”
这就涉及线程调度(Thread Scheduling)。
1. 谁在做调度?OS + JVM 双人舞
现实情况是这样的:
-
操作系统 负责真正的 CPU 调度:给哪个线程时间片、什么时候切线程。
-
JVM 只是:
- 把 Java 线程映射到 OS 线程
- 给 OS 一些“软建议”(比如线程优先级)
- 管理自己的线程对象、状态
你可以粗略理解为:
Java 把一堆线程交给系统说:
“哥们,这几个是我的,你按规则帮我轮着跑哈,我在旁边看状态就行。”
2. 调度策略:抢占式 + 时间片轮转
主流桌面/服务器操作系统(Windows、Linux 等)都是抢占式调度:
- 每个线程分配一个时间片(几十毫秒级别)。
- 用完或者被更高优先级线程抢占,就切出。
- 如果一个线程自己调用
sleep()/wait()/park(),会主动放弃 CPU。
Java 层面的 RUNNABLE 状态就是在 OS 调度层面:
- 要么是 Ready(等着)
- 要么是 Running(跑着)
3. 线程优先级:说是“优先”,其实只是“建议”📢
Java 提供了 10 个优先级:
public static final int MIN_PRIORITY = 1;
public static final int NORM_PRIORITY = 5;
public static final int MAX_PRIORITY = 10;
你可以这么设置优先级:
Thread high = new Thread(task, "high");
Thread low = new Thread(task, "low");
high.setPriority(Thread.MAX_PRIORITY);
low.setPriority(Thread.MIN_PRIORITY);
坑点在于:
- 优先级是对操作系统的“建议”,不是硬性规定。
- 不同 OS、不同 JVM 实现下表现差异很大。
- 过度依赖优先级实现“某个线程一定比另一个先执行”,基本就是在和运气谈恋爱。
实战建议:
除非特别明确的场景(比如某些后台低优先级线程),一般乖乖用默认优先级就行。
4. 哪些方法会影响调度?
几个常见的 Java API,本质就是在给调度器发信号:
(1)Thread.sleep(ms):我暂时不干活,让别人跑一会
- 当前线程进入
TIMED_WAITING,在这段时间里不会占用 CPU。 - 时间到了之后,回到 RUNNABLE 队列,等调度。
常用于:
- 定时轮询
- 降低 CPU 空转
- Demo 人为“放慢节奏”
(2)Thread.yield():我手头活不急,让同优先级的哥们先上
-
向调度器暗示:当前线程愿意让出 CPU。
-
实际上:
- 可能会让别的同优先级线程运行
- 也可能啥也不发生(调度器不理你)
简单说:
yield()是“请求让步”,不保证成功。
(3)join():我等你干完活再继续
Thread worker = new Thread(() -> doWork());
worker.start();
worker.join(); // 当前线程进入 WAITING / TIMED_WAITING
- 当前线程挂起,直到目标线程执行完毕(TERMINATED)或超时。
(4)wait()/notify():基于对象监视器的协作
wait():释放对象锁,进入 WAITING/TIMED_WAITINGnotify()/notifyAll():唤醒在该对象监视器上等待的线程
配合 synchronized 使用,是经典的生产者-消费者写法原始工具。
(5)LockSupport.park()/unpark():更底层的“停车/放行”
很多并发工具类(java.util.concurrent 包里的)都是用它构建的,例如:
AbstractQueuedSynchronizer(AQS)ReentrantLockCountDownLatch等
五、调度相关常见坑:CPU 炸、线程挂、活儿没人干 😵
1. 忙等(busy-wait):看起来很努力,实际上巨浪费
典型反例:
while (!condition) {
// 啥也不干,就一直转
}
- 这种写法会一直占着 CPU 时间片,拼命轮询。
- 在多核机器上,很容易把某个核打满。
更合理的写法应该是:
- 要么用
wait/notify - 要么用
LockSupport.park/unpark - 要么用
Condition.await/signal
例如:
synchronized (LOCK) {
while (!condition) {
LOCK.wait(); // 进入 WAITING,释放锁
}
}
2. 锁竞争严重导致线程大量 BLOCKED
如果你看到线程 dump 里一堆:
"worker-1" BLOCKED on ...
"worker-2" BLOCKED on ...
"worker-3" BLOCKED on ...
多半是某个 synchronized 块太粗:
public synchronized void handle() {
// 里面干活时间巨长
}
或者锁粒度过大,把不相关的操作都绑死在一个锁上。
解决思路:
- 缩小锁的粒度
- 用更细粒度的锁(分段锁)
- 使用并发容器 / CAS 等减少锁竞争
3. 线程饥饿(starvation):你永远轮不到
当某些线程一直拿不到锁 / 一直被高优先级线程“压着”,可能出现饥饿:
- 同一个锁总是被少数线程抢走
- 低优先级线程迟迟无法获得 CPU
避免饥饿的一些做法:
- 锁设计时尽量避免偏向某个固定线程
- 使用公平锁(如
new ReentrantLock(true)),但要注意性能代价 - 不要滥用线程优先级
4. 死锁(deadlock):相互等锁,大家一起躺平
虽然死锁不完全是“调度问题”,但最后表现就是:
- 多个线程一直 BLOCKED,互相拿着对方要的锁
- 整个系统部分逻辑完全停摆
经典例子:
public class DeadLockDemo {
private static final Object A = new Object();
private static final Object B = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (A) {
sleep(100);
synchronized (B) {
System.out.println("t1 got A and B");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (B) {
sleep(100);
synchronized (A) {
System.out.println("t2 got B and A");
}
}
});
t1.start();
t2.start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
产生死锁后:
- t1 拿着 A 等 B
- t2 拿着 B 等 A
- 谁也等不到,调度器也救不了你。
避免死锁的实战策略:约定统一的加锁顺序、减少嵌套锁、使用显式锁 + tryLock 等等。
六、实战推荐:怎么配合线程生命周期 & 调度写出不太作死的代码?🛠
1. 多数情况下,用线程池比自己管线程靠谱
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> {
// 你的任务逻辑
});
线程池的好处:
- 统一管理线程生命周期(创建、复用、销毁)
- 避免频繁 new Thread 带来的系统资源消耗
- 提供任务队列、拒绝策略等机制
你只管任务,线程的调度细节交给 JVM + OS + 线程池。
2. 不要用线程优先级实现“业务逻辑”
例如:
highPriorityThread.setPriority(Thread.MAX_PRIORITY);
lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
然后指望:
- “高优先级的线程一定先执行完”
- “低优先级一定最后跑”
大概率会翻车。
正确姿势:
- 用显式的任务队列、排队规则、锁、信号量等机制控制先后顺序。
- 把优先级当作系统调优的一个小参数,而非业务语义的一部分。
3. 正确处理中断:配合调度机制优雅停止线程
线程可能被别的线程调用 interrupt(),表示“建议你停一停”:
public class InterruptDemo {
public static void main(String[] args) throws Exception {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 模拟工作
Thread.sleep(1000);
System.out.println("working...");
} catch (InterruptedException e) {
// sleep 被中断会清除中断标志,这里要重新设置
Thread.currentThread().interrupt();
}
}
System.out.println("worker stopped");
});
worker.start();
Thread.sleep(2500);
worker.interrupt(); // 请求停止
}
}
- 中断配合
sleep/wait等阻塞操作,本质是在“调度层面优雅地停掉线程”。 - 合理处理中断,是写出“可控生命周期线程”的关键一步。
七、收个尾:线程是“活人”,不是“黑盒” 🧩
我们从头到尾,大致走了一遍:
-
线程生命周期:
- 从
NEW出生,到RUNNABLE干活, - 再到
BLOCKED/WAITING/TIMED_WAITING各种“排队排坑”, - 最后
TERMINATED收尾。
- 从
-
线程调度机制:
- OS + JVM 合作,抢占式 + 时间片轮转,
- 优先级只是“建议”,别迷信。
-
调度相关 API:
sleep、yield、join、wait/notify、park/unpark,- 背后都是在影响线程在不同状态之间切换。
-
常见坑:
- 忙等把 CPU 炸穿
- 锁竞争 & 饥饿
- 死锁导致一群线程一起躺平
-
实战建议:
- 多用线程池,少自己管线程;
- 不依赖优先级实现业务顺序;
- 正确处理中断;
- 把反复出问题的地方丢给并发工具类(
java.util.concurrent)
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)