线程基础与状态
1. 这是什么
线程是程序执行的最小调度单位。
学习并发的第一步,不是先看锁,而是先把线程本身的生命周期、调度方式和状态变化搞清楚。
一句话理解:
- 进程像“运行中的应用”
- 线程像“应用里真正执行任务的人”
2. 为什么重要
很多并发问题并不是锁本身导致的,而是对这些基础概念理解不清:
- 线程什么时候创建
- 线程什么时候阻塞
- 线程什么时候等待
- 为什么频繁创建线程代价高
- 为什么一个看起来“没做什么”的线程,也会占资源
基础越扎实,后面看线程池、同步器、死锁分析和性能排查就越稳。
3. 先建立直觉:进程、线程、并发、并行
3.1 进程和线程的区别
可以先用一个非常实用的理解:
- 进程:资源分配的基本单位
- 线程:CPU 调度执行的基本单位
一个进程通常会包含:
- 堆内存
- 方法区
- 打开的文件和网络资源
- 多个线程
线程之间:
- 共享进程内资源
- 但也有自己的程序计数器、栈等执行上下文
3.2 并发和并行的区别
- 并发:一段时间内“看起来一起做”,本质可能是快速切换
- 并行:同一时刻真的同时执行
在单核 CPU 上:
- 更常见的是并发,不是真并行
在多核 CPU 上:
- 既可能并发,也可能并行
工程上更重要的是:
- 你写并发程序时,重点不是纠结字面区别
- 而是理解多个执行流会互相影响、共享数据、争夺 CPU 和资源
4. 核心内容
4.1 Java 创建线程的常见方式
最常见的方式有三类:
- 继承
Thread - 实现
Runnable - 实现
Callable后配合FutureTask或线程池
现在更推荐的理解方式是:
- 任务和线程分离
- 用
Runnable/Callable描述任务 - 用线程或线程池执行任务
4.2 用户线程和守护线程
Java 线程分为:
- 用户线程
- 守护线程
区别是:
- 用户线程决定 JVM 是否继续存活
- 当只剩守护线程时,JVM 可以退出
典型守护线程例子:
- 垃圾回收相关线程
工程里要记住:
- 守护线程适合辅助性工作
- 不适合承载必须完成的核心业务逻辑
4.3 Java 线程状态有哪些
Java 里的线程状态主要有 6 种:
NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED
最重要的是不要死背名字,而要理解“它为什么进入这个状态”。
下面用一张状态流转图,把“线程是怎么从一个状态走到另一个状态的”串起来看:
可以这样理解这张图:
NEW -> RUNNABLE:线程对象创建后,只有调用start(),线程才真正进入可调度状态。RUNNABLE -> BLOCKED:这是“抢锁失败”,只和synchronized监视器锁竞争有关。RUNNABLE -> WAITING / TIMED_WAITING:这是线程主动进入等待,只是一个无限期等待,一个带超时等待。- 这张图按“概念理解”做了合并表达:等待条件满足后统一画回
RUNNABLE,表示线程重新回到可调度状态。 - 如果你从底层实现去看,
wait()/join()被唤醒后还要重新拿到监视器锁,这个过程可以理解为会短暂经过锁竞争,但在入门状态图里通常不单独展开。 RUNNABLE -> TERMINATED:线程执行结束后进入终止状态,不能再次start()。
4.4 每个状态怎么理解
NEW
- 线程对象刚创建
- 但还没有调用
start()
RUNNABLE
- 线程已经具备运行资格
- 可能正在运行,也可能正在等待 CPU 时间片
Java 把“就绪”和“运行中”统一归到 RUNNABLE
BLOCKED
- 线程正在等待进入
synchronized监视器锁 - 也就是“想进同步块,但锁被别人占着”
WAITING
- 线程进入无限期等待,等别人明确唤醒
- 常见来源:
Object.wait()、Thread.join()、LockSupport.park()
TIMED_WAITING
- 线程进入带超时的等待
- 常见来源:
sleep()、wait(timeout)、join(timeout)
TERMINATED
- 线程执行结束
4.5 sleep、wait、join 到底有什么区别
这是线程基础里最容易混淆的一组。
| 方法 | 属于谁 | 会不会释放锁 | 典型作用 |
|---|---|---|---|
Thread.sleep | Thread | 不会 | 让当前线程暂停一段时间 |
Object.wait | Object | 会 | 当前线程等待被唤醒,常配合同步块 |
Thread.join | Thread | 不用于协调锁释放 | 等待另一个线程执行完 |
最常见误区:
sleep不会释放已经持有的监视器锁wait必须在持有对象监视器的同步块中调用
4.6 为什么频繁创建线程代价高
线程不是“随便 new 一下就没成本”的轻量对象。
它的成本包括:
- 线程栈内存
- 操作系统线程创建和销毁开销
- 调度开销
- 上下文切换开销
这也是为什么生产环境通常不建议:
- 来一个任务就
new Thread(...)
而更推荐:
- 用线程池复用线程资源
4.7 上下文切换为什么会影响性能
当 CPU 从一个线程切到另一个线程时,需要保存和恢复执行现场,例如:
- 程序计数器
- 寄存器状态
- 栈相关上下文
如果线程过多、切换过于频繁,就会出现:
- CPU 真正干活时间变少
- 系统更多时间花在“切换”而不是“执行”上
5. 学习重点
这一章真正要掌握的是:
- 线程是执行单位,不是资源容器
- 线程状态要和具体行为对应起来理解
BLOCKED、WAITING、TIMED_WAITING是三个不同语义sleep、wait、join的用途和效果不同- 频繁创建线程有真实资源代价
6. 常见问题
6.1 把线程和进程混为一谈
这会导致你后面对:
- 资源共享
- 线程安全
- 内存模型
的理解都混乱。
6.2 不清楚线程何时进入阻塞或等待
例如很多人会把:
BLOCKEDWAITINGTIMED_WAITING
统称为“卡住了”,但它们背后的原因完全不同。
6.3 任务一多就直接 new Thread
这在简单 demo 里没问题,但在生产环境里通常不合适。
线程数量失控会带来明显的调度和资源压力。
6.4 误以为 sleep 会释放锁
不会。
线程 sleep 时,如果它已经拿到了 synchronized 锁,别人仍然进不来。
7. 动手验证
这一节可以直接复制运行,边看边验证。
7.1 准备一个可运行示例
新建文件 ThreadStateDemo.java,内容如下:
public class ThreadStateDemo {
private static void waitForState(Thread thread, Thread.State expected) throws InterruptedException {
for (int i = 0; i < 100; i++) {
if (thread.getState() == expected) {
return;
}
Thread.sleep(20);
}
}
public static void main(String[] args) throws Exception {
final Object lock = new Object();
final Object monitor = new Object();
Thread newThread = new Thread(() -> {}, "new-thread");
System.out.println("newState=" + newThread.getState());
Thread timedWaitingThread = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "timed-waiting-thread");
timedWaitingThread.start();
waitForState(timedWaitingThread, Thread.State.TIMED_WAITING);
System.out.println("timedWaitingState=" + timedWaitingThread.getState());
Thread lockHolder = new Thread(() -> {
synchronized (lock) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "lock-holder");
Thread blockedThread = new Thread(() -> {
synchronized (lock) {
System.out.println("blockedThreadEnteredLock");
}
}, "blocked-thread");
lockHolder.start();
Thread.sleep(100);
blockedThread.start();
waitForState(blockedThread, Thread.State.BLOCKED);
System.out.println("blockedState=" + blockedThread.getState());
Thread waitingThread = new Thread(() -> {
synchronized (monitor) {
try {
monitor.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "waiting-thread");
waitingThread.start();
waitForState(waitingThread, Thread.State.WAITING);
System.out.println("waitingState=" + waitingThread.getState());
Thread worker = new Thread(() -> {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "worker");
Thread joiner = new Thread(() -> {
try {
worker.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "joiner");
worker.start();
joiner.start();
waitForState(joiner, Thread.State.WAITING);
System.out.println("joinWaitingState=" + joiner.getState());
Thread daemonThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "daemon-thread");
daemonThread.setDaemon(true);
daemonThread.start();
System.out.println("daemonFlag=" + daemonThread.isDaemon());
synchronized (monitor) {
monitor.notifyAll();
}
timedWaitingThread.join();
lockHolder.join();
blockedThread.join();
waitingThread.join();
worker.join();
joiner.join();
System.out.println("terminatedState=" + joiner.getState());
}
}7.2 编译并运行
javac ThreadStateDemo.java
java ThreadStateDemo如果你在 PowerShell 中操作,也直接执行同样两条命令即可。
7.3 你应该观察到什么
输出不一定逐字完全一致,但应包含这些关键信息:
newState=NEW
timedWaitingState=TIMED_WAITING
blockedState=BLOCKED
waitingState=WAITING
joinWaitingState=WAITING
daemonFlag=true
terminatedState=TERMINATED中间还可能出现:
blockedThreadEnteredLock7.4 每一行在验证什么
newState=NEW:说明线程对象创建后、调用start()前处于NEWtimedWaitingState=TIMED_WAITING:说明sleep会让线程进入超时等待blockedState=BLOCKED:说明线程在等待进入synchronized锁时会阻塞waitingState=WAITING:说明wait()会让线程进入无限等待joinWaitingState=WAITING:说明join()的本质也是等待另一个线程结束daemonFlag=true:说明该线程被设置成守护线程terminatedState=TERMINATED:说明线程执行完后进入终止状态
7.5 再做两个延伸验证
你可以继续做下面两个实验:
- 在
lockHolder里把Thread.sleep(1000)改得更短或更长 - 把
waitingThread中的monitor.wait()改成monitor.wait(500)
你可以观察:
BLOCKED状态持续时间会变化WAITING会变成TIMED_WAITING
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 写一个 demo 对比
sleep、wait、join的行为差异 - 观察线程从
NEW到TERMINATED的关键状态变化 - 创建一批短生命周期线程,体会频繁创建线程的资源代价
- 分析一个业务线程为什么进入
BLOCKED或WAITING
9. 自测问题
- Java 线程常见状态有哪些?
BLOCKED和WAITING的核心区别是什么?- 守护线程和用户线程有什么区别?
- 为什么生产环境通常不建议频繁手动创建线程?
sleep、wait、join各自适合什么场景?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
- 线程是执行单位,进程是资源容器
RUNNABLE在 Java 里同时包含就绪和运行中的语义BLOCKED主要对应监视器锁竞争WAITING/TIMED_WAITING体现的是等待语义sleep不释放锁,wait需要在同步块中使用- 守护线程适合辅助任务,不适合承载必须完成的核心逻辑