Thread 类 API
终止模式
想象你正在做一个家务活,比如洗碗。突然妈妈叫你去吃饭,这时候你会怎么做?直接放下碗就走吗?当然不会,你会先把碗里的水倒掉,把碗放回原位,然后再去吃饭。这就是一种“优雅”的终止方式——先处理好当前的事情,再结束任务。
在编程中,线程就像你在做家务,有时候我们需要让线程停下来去做别的事情(比如吃饭)。如果我们直接强制停止线程(相当于直接扔下碗就走),可能会导致一些问题,比如资源没有释放干净,其他线程无法继续使用这些资源。
两阶段终止模式就是一种优雅地让线程停止的方法,它分为两个步骤:
- 通知线程该停止了:就像妈妈叫你去吃饭一样,告诉线程“你可以准备结束了”。
- 线程自己处理后事:线程收到通知后,会先完成手头的工作,释放资源,然后再真正停止。
错误的做法
- 使用
stop()方法:这就像直接把你从厨房里拽出来,不管碗还在不在水槽里。这样会导致资源泄露,其他线程可能永远无法使用这些资源。 - 使用
System.exit(int)方法:这就像直接关掉整个房子的电,不仅你不能继续洗碗,连其他人都不能做任何事情了。
终止模式之两阶段终止模式:Two Phase Termination
目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器
错误思想:
- 使用线程对象的 stop() 方法停止线程:stop 方法会真正杀死线程,如果这时线程锁住了共享资源,当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程:目的仅是停止一个线程,但这种做法会让整个程序都停止
正确的做法:两阶段终止模式
我们来看一个具体的例子,假设有一个监控线程,它的任务是每隔一段时间记录一些数据。现在我们需要让它停止工作。
package com.cg.jucproject.demo;
public class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start(); // 启动监控线程
Thread.sleep(3500); // 等待一段时间
tpt.stop(); // 停止监控线程
}
}
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) { // 检查是否被打断
System.out.println("后置处理");
break; // 结束循环
}
try {
Thread.sleep(1000); // 睡眠1秒
System.out.println("执行监控记录"); // 执行监控任务
} catch (InterruptedException e) { // 在睡眠期间被打断
e.printStackTrace();
// 重新设置打断标记,因为打断 sleep 会清除打断状态
thread.interrupt();
}
}
}
});
monitor.start();
}
// 停止监控线程
public void stop() {
monitor.interrupt(); // 通知线程该停止了
}
}
代码输出如下:

-
执行监控记录(出现3次):- 这是线程在正常工作。
main方法启动监控线程 (tpt.start()) 后,让它运行了3500毫秒(3.5秒)。 - 监控线程的循环里,每次
sleep(1000)睡1秒,醒来后打印执行监控记录。 - 3.5秒内,它完整地执行了3个周期(睡1秒 -> 打印 -> 睡1秒 -> 打印 -> 睡1秒 -> 打印),所以打印了3次。
- 这是线程在正常工作。
-
java.lang.InterruptedException: sleep interrupted...:- 当
main线程执行到tpt.stop()时,它调用了monitor.interrupt()。 - 此时,监控线程正在执行第4次的
Thread.sleep(1000),也就是它正处于“睡眠”状态。 - 关键点:对一个正在
sleep的线程调用interrupt(),会立即唤醒它,并抛出InterruptedException。 - 这个异常就是在
catch (InterruptedException e)块中被捕获并打印出来的。这不是错误,而是预期的行为,表明“中断信号”在sleep期间被接收到了。
- 当
-
后置处理:- 在
catch块中,代码执行了thread.interrupt();。这一步至关重要。- 为什么需要这一步?:当
sleep()因为被中断而抛出InterruptedException时,JVM 会自动将该线程的“中断状态”清除(即isInterrupted()会重新变成false)。如果不手动重新设置,下一次循环时if (thread.isInterrupted())就会是false,线程会继续循环下去,无法真正停止!
- 为什么需要这一步?:当
thread.interrupt();重新设置了中断状态。catch块执行完毕后,循环继续。进入下一次while(true)循环的开头,if (thread.isInterrupted())此时为true(因为刚被重新设置),于是执行System.out.println("后置处理");并break,线程优雅退出。
- 在
图示如下:

daemon(守护线程)
守护线程就像“服务员”,用户线程是“客人”。只要最后一个客人走了,不管服务员手头活干完没,都得关门下班!
什么是用户线程?什么是守护线程?
| 类型 | 英文名 | 大白话解释 |
|---|---|---|
| 用户线程 | User Thread | 你写的普通线程,比如 new Thread(() -> {...}),它是“主角”,JVM 会等它执行完才退出。 |
| 守护线程 | Daemon Thread | 它是“配角”、“后勤人员”,默默服务别人。一旦所有“主角”都走了,它就算没干完活,也得立刻停止,JVM 直接关门! |
关键规则(记住这句):
JVM 只有在还有用户线程运行时才会继续运行。一旦所有用户线程结束,不管守护线程有没有执行完,JVM 都会退出。
怎么设置守护线程?
用 setDaemon(true),必须在线程启动前设置!
Thread t = new Thread(() -> {
while (true) {
System.out.println("我是守护线程,我在默默工作...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
}
});
// 正确:启动前设置
t.setDaemon(true); // 标记为守护线程
t.start(); // 启动
错误写法:
t.start();
t.setDaemon(true); // 报错!线程已启动,不能再设为守护线程!
接下来写一个只有用户线程的demo(JVM 不会退出):
public class UserThreadDemo {
public static void main(String[] args) {
Thread userThread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("用户线程:第 " + i + " 次打印");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 不设置 daemon,默认就是用户线程
userThread.start();
System.out.println("main 线程结束了");
}
}
代码输出如下:

JVM 会等 userThread 打印完 5 次才退出。
再来写一个守护线程 + 用户线程的demo(守护线程会被强制结束):
public class DaemonThreadDemo {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) { // 无限循环,想一直打印
System.out.println("守护线程:我还活着...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
}
});
// 设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
// 模拟用户线程工作几秒就结束
Thread userThread = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("我是用户线程,我还在工作...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("用户线程:我干完活了,拜拜!");
});
userThread.start();
}
}
代码输出如下:

虽然 daemonThread 想一直打印,但 userThread 结束后,JVM 发现没有用户线程了,立刻终止守护线程,程序结束。所以你不会看到“守护线程:我还活着...”一直打印下去。
总结一下:
public final void setDaemon(boolean on):如果是 true ,将此线程标记为守护线程,线程启动前调用此方法。用户线程:平常创建的普通线程
守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示
说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去
常见的守护线程:
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
不推荐
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:
-
public final void stop():停止线程运行废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面
-
public final void suspend():挂起(暂停)线程运行废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁
-
public final void resume():恢复线程运行
线程原理
运行原理
Java Virtual Machine Stacks(Java 虚拟机栈):每个线程启动后,虚拟机就会为其分配一块栈内存
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
想象你是一个程序员,你每做一件事(比如写代码、开会、吃饭),都会在自己的笔记本上记下当前在做什么、变量是多少、下一步去哪。
在 Java 中:
每个线程就像一个独立的程序员,JVM 会给它发一个“私人笔记本” — 这就是“虚拟机栈”。
栈是怎么工作的?— “叠便签纸”
这个笔记本有个特点:你只能从最上面写和擦(后进先出,LIFO)。
- 每当你调用一个方法,比如
main()调用a(),a()调用b():- JVM 就给你这个线程的栈压入一张新的“便签纸”,这张纸就叫 栈帧(Stack Frame)。
- 每张便签纸上写着:
- 这个方法的局部变量(比如
int n = 10;) - 方法执行到哪一行了(返回地址)
- 中间计算用的临时数据(操作数栈)
- 这个方法的局部变量(比如
public static void main(String[] args) {
a();
}
static void a() {
int x = 10;
b();
}
static void b() {
int y = 20;
System.out.println(y);
}
线程执行过程:
| 栈顶(当前正在执行) | 栈内容(从上到下) |
|---|---|
b() 的栈帧(y=20) | ← 活动栈帧(唯一一个正在执行的) |
a() 的栈帧(x=10) | |
main() 的栈帧 | |
| ... |
每个线程只能有一个“活动栈帧”:就是当前正在执行的方法(这里是 b())。
其他方法都“暂停”在下面,等上面的执行完再继续。
线程上下文切换(Thread Context Switch):一些原因导致 CPU 不再执行当前线程,转而执行另一个线程
- 线程的 CPU 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park 等方法
线程上下文切换 — CPU 的“换人上岗”
你是一个 CPU,同一时间只能干一件事。
现在有多个线程(多个任务)排队等你处理。你不能一直干一个活,得轮流来。
上下文切换 = 你从干 A 的活,换成干 B 的活。
但换人之前,你得:
- 把 A 的工作进度、笔记、状态都收好打包(保存)
- 把 B 的包拿出来,恢复他的笔记和进度
- 开始干 B 的活
这个“收包 + 换包”的过程,就是 上下文切换。
什么时候会发生上下文切换?
| 原因 | 解释 |
|---|---|
| CPU 时间片用完 | A 干了 10ms,系统说:“你歇会儿,让别人干干。” |
| 垃圾回收(GC) | JVM 突然要打扫内存,所有线程暂停,GC 线程上场。 |
| 高优先级线程来了 | 领导来了,普通员工先停下,让领导先办。 |
| 线程自己睡了 | A 自己调了 sleep() 或 wait(),说:“我困了,先歇会儿。” |
切换时要保存啥?
操作系统会把当前线程的“全部家当”存到 PCB(进程控制块) 里,包括:
- 程序计数器(PC Register):记住下一条要执行的指令地址(就像书签)
- 虚拟机栈的所有栈帧:每个方法的局部变量、返回地址等
- 寄存器状态、堆栈指针等
换回来时,再把这些“家当”还给它,接着干。
程序计数器(Program Counter Register):记住下一条 JVM 指令的执行地址,是线程私有的
当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
程序计数器 — 线程的“书签”
你正在看书(执行代码),看到第 38 页第 5 行。
突然你妈叫你吃饭,你合上书,放个书签在第 38 页。
吃完饭回来,你一看书签,就知道从哪继续看。
程序计数器就是线程的“书签”!
- 它记录:下一条要执行的 JVM 字节码指令的地址。
- 每个线程都有自己的程序计数器(线程私有)。
- 只有执行 Java 方法时,它才记录地址;执行 native 方法时,它的值是 undefined。
JVM 规范并没有限定线程模型,以 HotSopot 为例:
- Java 的线程是内核级线程(1:1 线程模型),每个 Java 线程都映射到一个操作系统原生线程,需要消耗一定的内核资源(堆栈)
- 线程的调度是在内核态运行的,而线程中的代码是在用户态运行,所以线程切换(状态改变)会导致用户与内核态转换进行系统调用,这是非常消耗性能
HotSpot 的线程模型(1:1 模型)— Java 线程与操作系统线程
Java 的线程不是“虚拟”的,它是真实映射到操作系统线程的。
一个 Java 线程 = 一个操作系统原生线程(1:1 模型)
这就像是:
- 你公司招一个程序员(Java 线程)
- 操作系统就真给这个人发工牌、安排工位(内核线程)
缺点:切换太贵!
因为 Java 线程直接对应操作系统线程,所以:
- 线程调度由操作系统内核负责(在“内核态”运行)
- 但你的代码是在“用户态”运行
- 每次切换线程,就要从“用户态”进入“内核态”,做系统调用
这个过程非常耗性能!就像:
你想让两个员工换班,得先叫人事经理(内核) 来登记、交接、签字,非常麻烦。
所以:
- 线程不能创建太多(否则系统忙于切换,真正干活的时间少了)
- 这也是为什么后来有了 协程(如 Kotlin 协程、Quasar),它们是“用户态线程”,切换更快。
Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程
public class Main {
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("我是子线程");
});
t.start(); // 启动子线程
System.out.println("main线程结束");
}
}
main()方法启动的是一个 进程,同时也是这个进程里的 主线程。- 在
main中创建的线程(如t),叫 子线程。 main线程是这些子线程的 父线程。
注意:main 线程结束,程序不一定结束!
只要还有用户线程在运行(比如 t 是普通线程),JVM 就不会退出。
但如果 t 是守护线程,main 结束后,JVM 会立刻退出,t 也会被终止。
线程调度
线程调度指系统为线程分配处理器使用权的过程,方式有两种:协同式线程调度、抢占式线程调度(Java 选择)
想象你是一个 CPU,特别能干,但同一时间只能做一件事。
现在有好几个线程(任务)排队等你处理,比如:
- 线程A:下载电影
- 线程B:播放音乐
- 线程C:打游戏
你不能同时干三件事,那怎么办?系统(操作系统)要决定:先让谁上,干多久,什么时候换人。这个“安排谁上场干活”的过程,就叫 线程调度。
协同式线程调度:线程的执行时间由线程本身控制
- 优点:线程做完任务才通知系统切换到其他线程,相当于所有线程串行执行,不会出现线程同步问题
- 缺点:线程执行时间不可控,如果代码编写出现问题,可能导致程序一直阻塞,引起系统的奔溃
协同式调度 — “自觉交接班制”
公司规定:谁干活,自己说了算。
你干完了,或者你觉得该歇了,就主动喊一声:“我干完了,下一个上!”
这就是 协同式调度:线程自己控制执行时间,自己决定啥时候让出 CPU。
优点:
- 不用管理员(系统)操心,大家自己协调。
- 没有“抢活干”的冲突,不会出现同步问题(比如两个人同时改同一个文件)。
缺点:
- 万一有人不自觉呢?
- 比如你打游戏上头了,说:“再玩一局,就一局!” 结果玩了10局……
- 那你弟就一直等着,音乐也停了,下载也卡了。
- 整个系统就卡死了!
所以这种调度方式风险高,Java 没采用它。
抢占式线程调度:线程的执行时间由系统分配
- 优点:线程执行时间可控,不会因为一个线程的问题而导致整体系统不可用
- 缺点:无法主动为某个线程多分配时间
抢占式调度 — “定时打卡换班制”
公司现在改制度了:每人最多干10分钟,时间一到,不管干没干完,都必须下台!
谁说了算?管理员(操作系统)说了算!
这就是 抢占式调度:执行时间由系统分配,时间一到,强行换人。
优点:
- 公平!可控!
- 即使你写了个死循环(
while(true)),系统也会强行把你“踢下去”,不会让整个程序卡死。 - 系统稳定,不会因为一个线程出问题就崩了。
缺点:
- 你不能说:“这任务特别重要,让我多干会儿。”
- 系统只认时间片,不认“感情”,无法主动给某个线程更多时间。
Java 用的就是这种调度方式!
Java 提供了线程优先级的机制,优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它。在线程的就绪状态时,如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用
说明:并不能通过优先级来判断线程执行的先后顺序
虽然不能“加时赛”,但 Java 提供了一个“小建议”机制:线程优先级。
你有两个任务:
- 任务A:紧急 bug 修复(优先级高)
- 任务B:日常打扫(优先级低)
你跟管理员说:“能不能多照顾一下 A?”
管理员说:“行,我尽量。”
但注意:这只是个‘建议’,不是命令!
管理员(操作系统)可以听,也可以不听。
- 当 CPU 很忙时(任务多),优先级高的线程会获得更多时间片,大概率先执行。
- 当 CPU 很闲时(就两个线程),优先级几乎没区别,反正都有时间干。
- 不能靠优先级判断谁先执行! 因为最终决定权在操作系统。
Thread t1 = new Thread(() -> System.out.println("高优先级"));
Thread t2 = new Thread(() -> System.out.println("低优先级"));
t1.setPriority(Thread.MAX_PRIORITY); // 10
t2.setPriority(Thread.MIN_PRIORITY); // 1
t1.start();
t2.start();
输出顺序不确定!可能是 t1 先,也可能是 t2 先。优先级只是“提示”。
未来优化
内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多个用户线程映射到一个内核级线程),被设计为协同式调度,所以叫协程
- 有栈协程:协程会完整的做调用栈的保护、恢复工作,所以叫有栈协程
- 无栈协程:本质上是一种有限状态机,状态保存在闭包里,比有栈协程更轻量,但是功能有限
为什么需要协程?——因为“内核线程太贵了!”
协程是一种用空间换时间、用用户态换内核态的聪明设计。
你开了一家公司(程序),想让员工(线程)干活。但公司有个规定:每个员工都必须配一个独立办公室、工位、电脑、人事档案(内核资源)。这就像 Java 的 内核级线程(1:1 模型)。
问题来了:你想招 1万个 员工?不行!办公室不够,电脑买不起,人事系统也扛不住!
而且每次换班(上下文切换),还得人事经理(操作系统内核)来登记、交接、签字,特别慢、特别累。所以大家想:能不能搞一种“轻量员工”?不用独立办公室,换班也不用找人事?
这就是 协程(Coroutine) 的由来!
什么是协程?——“灵活的临时工”
协程 = 用户自己管理的“轻量线程”它不是操作系统管的,而是程序员自己在代码里调度的。
- 多个协程可以跑在同一个内核线程上(多对一模型)。
- 它的调度由用户程序自己控制,不经过操作系统内核。
- 切换快、开销小,能轻松创建成千上万个!
| 类型 | 内核线程 | 协程 |
|---|---|---|
| 身份 | 正式员工,签合同,有工牌 | 临时帮工,老板叫谁上谁上 |
| 办公位 | 独立工位(内核栈) | 共用一张桌子(用户空间) |
| 换班 | 要人事登记(系统调用) | 老板一句话:“小王上,小李歇会儿” |
| 成本 | 高(资源多) | 低(几乎不占系统资源) |
所以协程特别适合:高并发、大量 I/O(比如 Web 服务器处理上万请求)。
协程是“协同式调度”——自己主动让位
- 抢占式:时间一到,强制换人(Java 线程)
- 协同式:自己干完活,说“我好了,你上吧”(协程)
协程是协同式调度:它不会被系统强行中断,而是自己主动让出 CPU。
// Kotlin 协程例子
launch {
println("第一步")
delay(1000) // 主动让出,不占用 CPU
println("第二步")
}
比如在Kotlin中,delay(1000) 不是 sleep,它会主动挂起协程,让其他协程运行,1秒后再恢复。
两种协程:有栈 vs 无栈
有栈协程它像一个“完整的迷你线程”,有自己的独立记事本(调用栈)。
- 能在任何函数中暂停、恢复。
- 支持复杂的调用链:A → B → C → 暂停 → 恢复 → D → E
理解:
你正在写代码,写到一半,老板说:“去接个电话。”你把当前所有变量、代码行、思路都记在私人笔记本上,合上本子去接电话。回来后,打开本子,接着写。这个“笔记本”就是它的私有栈。
缺点:
- 每个协程都要分配栈内存(比如 1MB),虽然比线程小,但多了也吃不消。
无栈协程没有独立的调用栈,状态保存在闭包(Closure)里,本质上是一个状态机。
- 不能在任意位置暂停,只能在
suspend函数处挂起。 - 状态用对象保存,比如“当前在第几步”。
举个简单例子(JavaScript 风格):
function* myCoroutine() {
console.log("第一步");
yield; // 暂停
console.log("第二步");
}
它内部会被编译成:
class StateMachine {
int state = 0;
void resume() {
switch(state) {
case 0:
System.out.println("第一步");
state = 1;
return;
case 1:
System.out.println("第二步");
state = 2;
return;
}
}
}
看,它没有“栈”,只有
state变量记录进度。
优点:
- 极其轻量!只用几个字节保存状态。
- 创建百万个也不怕。
缺点:
- 功能受限,不能在任意函数中挂起。
- 调试复杂(代码被编译成状态机)。
有栈协程中有一种特例叫纤程,在新并发模型中,一段纤程的代码被分为两部分,执行过程和调度器:
- 执行过程:用于维护执行现场,保护、恢复上下文状态
- 调度器:负责编排所有要执行的代码顺序
什么是纤程(Fiber)?——有栈协程的“升级版”
纤程 = 有栈协程 + 更智能的调度器
它把一段代码分成两部分:
1. 执行过程(Execution)——“干活的人”
- 负责维护执行现场:保存局部变量、调用栈、PC 寄存器等。
- 就像一个“可暂停的函数执行器”。
2. 调度器(Scheduler)——“包工头”
- 负责安排哪个纤程先执行、什么时候切换。
- 可以是单线程调度,也可以是多线程池调度。
举个例子。你是个包工头(调度器),手下有一堆工人(纤程)。
- 工人A在砌墙,砌到一半说:“我累了,歇会儿。”
- 你记下他干到哪了(保存上下文),让他休息。
- 让工人B去刷漆。
- 过会儿,你再叫工人A回来,给他笔记本:“接着砌吧。”
这就是纤程的“执行 + 调度”分离。
线程状态
进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态
线程由生到死的完整过程(生命周期):当线程被创建并启动以后,既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 java.lang.Thread.State 这个枚举中给出了六种线程状态:
| 线程状态 | 导致状态发生条件 |
|---|---|
| NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 |
| Runnable(可运行) | 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) |
| Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 |
| Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 |
| Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait |
| Teminated(结束) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 |

对这张图的解释:
-
NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE
-
RUNNABLE <--> WAITING:
-
调用 obj.wait() 方法时
调用 obj.notify()、obj.notifyAll()、t.interrupt():
- 竞争锁成功,t 线程从 WAITING → RUNNABLE
- 竞争锁失败,t 线程从 WAITING → BLOCKED
-
当前线程调用 t.join() 方法,注意是当前线程在 t 线程对象的监视器上等待
-
当前线程调用 LockSupport.park() 方法
-
-
RUNNABLE <--> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n)
-
RUNNABLE <--> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败
也可以写一个简单的代码demo理解:
public class ThreadStateDemo {
// 共享锁对象,用于线程同步
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
System.out.println("程序开始:主线程启动");
// 1. NEW → RUNNABLE:创建并启动线程
Thread threadT1 = new Thread(() -> {
synchronized (lock) {
System.out.println("线程T1:已进入同步块,当前状态为 RUNNABLE");
try {
System.out.println("线程T1:即将调用 wait(),进入 WAITING 状态...");
lock.wait(); // 释放锁,进入 WAITING
System.out.println("线程T1:被唤醒,重新获得锁,状态回到 RUNNABLE");
} catch (InterruptedException e) {
System.out.println("线程T1:等待期间被中断");
}
}
}, "Thread-T1");
System.out.println("线程T1:刚创建,状态为 NEW");
threadT1.start();
System.out.println("线程T1:已调用 start(),状态变为 RUNNABLE");
// 等待确保 t1 进入 WAITING
Thread.sleep(1000);
// 演示 TIMED_WAITING
Thread threadT2 = new Thread(() -> {
System.out.println("线程T2:开始执行,状态为 RUNNABLE");
try {
System.out.println("线程T2:即将调用 sleep(3000),进入 TIMED_WAITING 状态...");
Thread.sleep(3000);
System.out.println("线程T2:sleep 结束,状态回到 RUNNABLE");
} catch (InterruptedException e) {
System.out.println("线程T2:sleep 期间被中断");
}
}, "Thread-T2");
threadT2.start();
// 演示 BLOCKED 状态
Thread threadT3 = new Thread(() -> {
System.out.println("线程T3:尝试获取锁...");
synchronized (lock) {
System.out.println("线程T3:成功获取锁,状态为 RUNNABLE(之前可能为 BLOCKED)");
System.out.println("线程T3:执行完毕");
}
}, "Thread-T3");
threadT3.start(); // 此时 lock 被 threadT1 持有,t3 会 BLOCKED
Thread.sleep(500); // 让 t3 尝试获取锁
// 主线程唤醒 t1
synchronized (lock) {
System.out.println("主线程:准备唤醒在锁上等待的线程T1");
lock.notify();
System.out.println("主线程:已调用 notify()");
}
// 等待所有线程完成
threadT1.join();
threadT2.join();
threadT3.join();
System.out.println("所有线程均已执行完毕,状态为 TERMINATED");
System.out.println("程序结束");
}
}
| 转换 | 说明 |
|---|---|
| NEW → RUNNABLE | 调用 start() 方法后,线程进入就绪状态,等待 CPU 调度 |
| RUNNABLE → WAITING | 调用 wait() 后,线程释放锁并无限等待,直到被 notify() 或 interrupt() |
| WAITING → RUNNABLE | 被 notify() 唤醒后,尝试重新获取锁,成功后继续执行 |
| WAITING → BLOCKED | 被唤醒后,若其他线程持有锁,则进入 BLOCKED 状态等待锁 |
| RUNNABLE → TIMED_WAITING | 调用 sleep(n)、wait(n)、join(n) 等带超时的方法 |
| TIMED_WAITING → RUNNABLE | 超时结束或被提前中断 |
| RUNNABLE → BLOCKED | 尝试进入 synchronized 块/方法,但锁被其他线程持有 |
| BLOCKED → RUNNABLE | 成功获取锁后,进入运行状态 |
查看线程
Windows:
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
Linux:
- ps -ef 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p 查看某个进程(PID)的所有线程
Java:
- jps 命令查看所有 Java 进程
- jstack 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
Java线程原理与状态解析

被折叠的 条评论
为什么被折叠?



