主题
线程基础与生命周期:从 Process 到 Thread
本文是 Java 并发系统学习系列的 L1 入门篇。前置:无,会写 Java 就能读。 学完可以配合面试题食用:线程状态流转与上下文切换开销
为什么要有线程:从进程说起
操作系统最初只有进程。每个进程有独立的地址空间、文件描述符等资源,好处是隔离彻底——一个进程崩了不连累别人;代价是"并发"的成本很高:创建进程要复制整套资源,切换进程要换页表、刷 TLB,一次上下文切换在 ~10μs 量级(进程),而线程切换只要 ~1μs 量级。
线程就是"进程里的一条执行流":同进程内的多个线程共享堆和方法区(所以能直接共享对象),只各自保留一份栈和寄存器上下文。共享带来两个直接后果,也是整个 Java 并发的主线:
- 好处:通信几乎零成本,一个线程写进 HashMap,另一个线程立刻"看得到地址"
- 坏处:多个线程同时改同一块内存,就会出乱子——这就是后面要讲的原子性、可见性、有序性三大问题
所以学 Java 并发的第一步不是背 API,而是建立"共享内存 + 多条执行流"的心智模型。
创建线程的三种方式与本质区别
java
// 1. 继承 Thread:把"任务"和"执行者"焊死在一个类里
class MyThread extends Thread {
@Override public void run() {
System.out.println("running in " + getName());
}
}
// 2. 实现 Runnable:任务是任务,线程是线程(推荐,也更贴近线程池用法)
Runnable task = () -> System.out.println("running in " + Thread.currentThread().getName());
new Thread(task, "t-2").start();
// 3. Callable + FutureTask:任务有返回值、能抛受检异常
FutureTask<Integer> ft = new FutureTask<>(() -> { Thread.sleep(100); return 42; });
new Thread(ft, "t-3").start();
System.out.println(ft.get()); // 阻塞直到拿到 42本质区别一句话:Thread 是执行的载体,Runnable/Callable 才是任务本身。三种方式殊途同归——最后都是 new 一个 Thread、把任务塞给它。区别只在耦合度(继承占了唯一的继承位)和任务形态(Runnable 无返回值,Callable 有返回值且能抛异常)。工程里几乎不直接 new Thread,而是把 Callable/Runnable 丢给线程池,但池化的底层仍然是这一套。
线程的六种状态与流转
Thread.State 枚举定义了 6 种状态,这是排查并发问题的"坐标系",jstack 线程 dump 里的每一个线程都挂着其中一种:
- NEW:对象建好了,还没调 start()
- RUNNABLE:就绪或正在执行(Java 不区分"等 CPU"和"在 CPU 上跑",统称 RUNNABLE)
- BLOCKED:等监视器锁(synchronized),抢到就回 RUNNABLE
- WAITING:无限期等待,如
Object.wait()、Thread.join()、LockSupport.park() - TIMED_WAITING:限时等待,如
sleep(n)、wait(n)、join(n) - TERMINATED:run() 跑完
流转关系一图看全:
mermaid
stateDiagram-v2
[*] --> NEW: new Thread()
NEW --> RUNNABLE: start()
RUNNABLE --> TERMINATED: run() 结束
RUNNABLE --> BLOCKED: 等 synchronized 锁
BLOCKED --> RUNNABLE: 抢到锁
RUNNABLE --> WAITING: wait()/join()/park()
WAITING --> RUNNABLE: notify()/join 返回/unpark()
RUNNABLE --> TIMED_WAITING: sleep(n)/wait(n)/join(n)
TIMED_WAITING --> RUNNABLE: 超时到期或被唤醒
TERMINATED --> [*]两个高频易混点:WAITING/BLOCKED 的区别是"等通知"vs"等锁"(park 进 WAITING 但等 synchronized 进 BLOCKED);以及 Java 层面看不到 IO 阻塞——线程卡在 socket read 时状态仍是 RUNNABLE,这是 jstack 排查时容易误判的地方。
start、interrupt 与守护线程
start() vs run():调 start() 是让 JVM 起一条新执行流去跑 run();直接调 run() 只是普通方法调用,全程在当前线程里执行,毫无并发。start() 只能调一次,第二次抛 IllegalThreadStateException——状态机里 TERMINATED 没有回边。
interrupt 的正确姿势:interrupt() 不会"杀"线程,它只做两件事:把中断标志置 true;若线程正阻塞在 sleep/wait/join 上,则打断阻塞并抛 InterruptedException(同时清掉标志)。所以正确退出姿势是"任务配合检查",而不是指望外部强杀:
java
Thread slow = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) { // 每轮检查标志位
// ...干活...
}
System.out.println("检测到中断,正常收尾退出");
});
slow.start();
Thread.sleep(100);
slow.interrupt(); // 只设标志,不暴力停止catch 到 InterruptedException 后要么重新 Thread.currentThread().interrupt() 保住标志,要么赶紧返回,吞掉不处理是最差的。
守护线程(daemon):setDaemon(true) 要在 start() 前调。JVM 判断"所有非守护线程结束"就退出,守护线程被直接抛弃——连 finally 都不保证执行。适合后台心跳、Metrics 上报这类"死了无所谓"的活;数据库连接清理、日志落盘这类有副作用的收尾千万别放守护线程。
动手实操:亲手摸一遍状态转换
跑一遍下面这个程序,把 6 种状态打出 5 种来(NEW / TIMED_WAITING / BLOCKED / WAITING / TERMINATED),对着上面的流转图逐个对应"是哪条边把它带过去的":
java
public class ThreadStateDemo {
public static void main(String[] args) throws Exception {
Object lock = new Object();
Thread worker = new Thread(() -> {
try {
synchronized (lock) {
Thread.sleep(300); // 持锁睡眠 -> TIMED_WAITING(sleep 不释放锁)
lock.wait(); // 释放锁、无限期等待 -> WAITING
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 保住中断标志
}
});
System.out.println("创建后未 start: " + worker.getState()); // NEW
worker.start();
Thread.sleep(100); // 等 worker 抢到锁并进入 sleep
System.out.println("持锁 sleep 中: " + worker.getState()); // TIMED_WAITING
Thread blocker = new Thread(() -> {
synchronized (lock) {} // 锁被 worker 持着 -> 排队等锁
});
blocker.start();
Thread.sleep(50);
System.out.println("等监视器锁: " + blocker.getState()); // BLOCKED
synchronized (lock) { lock.notifyAll(); } // 唤醒 worker(此时自己也短暂 BLOCKED)
worker.join();
blocker.join();
System.out.println("run() 结束后: " + worker.getState()); // TERMINATED
}
}关键行注释都标了边:sleep(300) 期间锁没放,blocker 和 main 都在 BLOCKED 排队;worker 进入 wait() 释放锁后它们才有机会拿锁。改几个毫秒数重跑,观察打印顺序变化,比背十遍状态图印象都深。
常见误区与小结
- 误区一:以为调 run() 也能并发——只是普通方法调用,代码全在主线程跑。
- 误区二:以为 interrupt() 能停线程——它只设标志/打断阻塞点,任务不检查标志就永远停不下来。
- 误区三:吞掉 InterruptedException 什么都不做——上层调用者失去感知中断的能力,正确做法是恢复标志或快速返回。
- 误区四:守护线程里放清理逻辑——JVM 退出时守护线程被直接抛弃,finally 不保证执行。
- 误区五:以为存在 RUNNING 状态——Java 的 RUNNABLE 涵盖"等 CPU"和"执行中",IO 阻塞时也显示 RUNNABLE。
小结:本文打了两块地基——"共享内存 + 多执行流"的心智模型,和 6 种状态的状态机。前者解释了并发 bug 为什么发生,后者是后面用 jstack/Arthas 排查问题的通用语言。下一篇我们正式进入并发三大问题(原子性、可见性、有序性),并引出 Java 内存模型(JMM)——那才是真正"难"但值钱的部分。
参考
参考:JDK
java.lang.Thread/Thread.State源码与 javadoc;《Java 并发编程实战》(JCIP)第 1 章;OpenJDK Thread.java。