线程基础详解
场景:周一早上,飞翔科技的办公区一片忙碌。大翔拿着一份需求文档走到白歌面前:"白歌,我们新的薪资系统要能同时处理几千名员工的工资计算。小崔写的单线程版本跑一次得两小时,财务部都快疯了。"
白歌(推了推眼镜):"多线程啊。你先把线程生命周期搞明白,不然代码上线后死锁了可别找我。"
小崔(从工位探出头):"线程不就是 Thread.start() 吗?我用了啊!"
大翔:"你那是 Thread.start() 里调了 run() 吧,根本没异步。"
朱璐(拿着一杯咖啡悠悠飘过):"多线程可不是Thread.start一下那么简单的事情啦!线程状态、中断、join配合,每一步都有坑。"
Frank(从会议室出来):"Guys, let's also consider thread pools. But first, master the basics — lifecycle, interrupts, and the difference between start() and run()."
一、定义速查表
| 概念 | 定义 | 关键方法/状态 |
|---|---|---|
| 线程(Thread) | 程序中执行的独立路径,是 CPU 调度的最小单位 | Thread类、Runnable接口、Callable<V>接口 |
| Thread 类 | 继承 java.lang.Thread,重写 run() 方法 | start(), run(), join(), sleep(), yield(), interrupt() |
| Runnable 接口 | 实现 java.lang.Runnable,解耦任务与线程 | void run() |
| Callable 接口 | 类似 Runnable,但有返回值且可抛异常 | V call() throws Exception |
| start() vs run() | start() 启动新线程调用 run();直接调用 run() 在主线程执行 | start() 创建新栈帧,run() 复用当前栈帧 |
| 线程生命周期 | NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED | 6 种状态,见下文状态图 |
| sleep() | 静态方法,让当前线程休眠指定毫秒,不释放锁 | Thread.sleep(long millis) |
| join() | 等待目标线程终止,底层用 wait() 实现 | thread.join(), join(long millis) |
| yield() | 提示调度器当前线程愿意让出 CPU,不保证 | Thread.yield() |
| interrupt() | 设置线程的中断标志位;若线程在 wait/sleep/join 中则抛 InterruptedException | thread.interrupt(), Thread.interrupted(), isInterrupted() |
| 守护线程(Daemon) | JVM 在所有非守护线程结束后自动退出,不等待守护线程 | thread.setDaemon(true) |
| 线程优先级 | 1~10,默认 5,仅作为调度提示,不保证顺序 | setPriority(int), MIN_PRIORITY=1, MAX_PRIORITY=10 |
二、线程生命周期 —— Mermaid 状态图
状态详解(JDK 8 java.lang.Thread.State 枚举):
| 状态 | 含义 | 进入方式 | 退出方式 |
|---|---|---|---|
| NEW | 线程对象已创建,但尚未调用 start() | new Thread() | start() 调用后进入 RUNNABLE |
| RUNNABLE | 可运行的,包括正在执行和等待 CPU 调度 | start() 后 | 可能进入 BLOCKED/WAITING/TERMINATED |
| BLOCKED | 等待进入 synchronized 块/方法,等待监视器锁 | 尝试获取已被占用的对象锁 | 获取到锁后进入 RUNNABLE |
| WAITING | 无限期等待另一个线程的通知 | wait(), join(), LockSupport.park() | notify()/notifyAll(),被 join 线程终止,unpark() |
| TIMED_WAITING | 带超时的等待 | sleep(ms), wait(ms), join(ms), parkNanos() | 超时到期或收到通知 |
| TERMINATED | 线程执行完毕或异常退出 | run() 方法返回或抛出未捕获异常 | 不可恢复,线程对象可被 GC |
三、Thread 类 vs Runnable 接口 —— 完整对比
| 维度 | 继承 Thread 类 | 实现 Runnable 接口 |
|---|---|---|
| 耦合度 | 高,任务与线程绑定 | 低,任务与线程分离,可提交给线程池 |
| 扩展性 | 单继承限制,无法再继承其他类 | 可实现多接口,灵活扩展 |
| 资源共享 | 需要 static 变量或外部对象 | 同一个 Runnable 实例可被多个线程共享 |
| 线程池兼容 | 不兼容(每个 new Thread() 都是独立线程) | 兼容,Runnable 可直接提交给 ExecutorService |
| 返回值 | run() 无返回值 | run() 无返回值,需 Callable 获取返回值 |
| 使用建议 | 不推荐,除非必须扩展 Thread 类的特殊行为 | 推荐,面向接口编程 |
四、完整可运行示例
示例 1:飞翔科技多线程处理员工工资计算
场景:飞翔科技有 1000 名员工,财务部需要计算每人当月工资(含基本工资+绩效+加班)。大翔用多线程分片计算。
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 飞翔科技薪酬计算系统 — 多线程分片处理员工工资
*
* 场景:1000 名员工,分 5 个线程并行计算,最后汇总
*/
public class SalaryCalculator {
// 员工实体
static class Employee {
int id;
String name;
double baseSalary; // 基本工资
double performance; // 绩效
double overtime; // 加班费
double total; // 合计
public Employee(int id, String name, double baseSalary) {
this.id = id;
this.name = name;
this.baseSalary = baseSalary;
}
public void calculate() {
// 模拟计算逻辑:绩效 = 基本工资 * 随机系数(0.8~1.5)
this.performance = baseSalary * (0.8 + Math.random() * 0.7);
// 加班费 = 基本工资 * 0.3 * 随机
this.overtime = baseSalary * 0.3 * Math.random();
this.total = baseSalary + performance + overtime;
// 模拟计算耗时
try { Thread.sleep(1); } catch (InterruptedException e) { }
}
@Override
public String toString() {
return String.format("员工[%d] %s: 基本=%.0f, 绩效=%.0f, 加班=%.0f, 合计=%.0f",
id, name, baseSalary, performance, overtime, total);
}
}
// 工资计算线程 — 继承 Thread
static class SalaryThread extends Thread {
private final List<Employee> employees;
private final int threadId;
public SalaryThread(List<Employee> employees, int threadId) {
this.employees = employees;
this.threadId = threadId;
}
@Override
public void run() {
System.out.printf("[线程-%d] 开始处理 %d 名员工的工资...\n",
threadId, employees.size());
long start = System.currentTimeMillis();
for (Employee emp : employees) {
emp.calculate();
}
long end = System.currentTimeMillis();
System.out.printf("[线程-%d] 处理完成,耗时: %d ms\n",
threadId, (end - start));
}
}
public static void main(String[] args) throws InterruptedException {
// 1. 生成 1000 名员工数据
List<Employee> allEmployees = new ArrayList<>();
String[] names = {"张伟", "李娜", "王芳", "赵强", "刘洋", "陈静", "杨帆", "黄莉",
"周明", "吴秀", "徐涛", "孙华", "马良", "朱峰", "胡波", "林雨",
"何雪", "郭瑞", "高洁", "罗平"};
Random random = new Random();
for (int i = 0; i < 200; i++) {
String name = names[i % names.length] + (i + 1);
double base = 5000 + random.nextInt(15000); // 5k~20k 基本工资
allEmployees.add(new Employee(i + 1, name, base));
}
System.out.println("========== 飞翔科技薪酬计算系统 (多线程版) ==========");
System.out.println("总员工数: " + allEmployees.size());
System.out.println();
// 2. 分片:分成 5 个线程
int threadCount = 5;
int chunkSize = allEmployees.size() / threadCount;
SalaryThread[] threads = new SalaryThread[threadCount];
long totalStart = System.currentTimeMillis();
// 3. 创建并启动线程
for (int i = 0; i < threadCount; i++) {
int fromIndex = i * chunkSize;
int toIndex = (i == threadCount - 1) ? allEmployees.size() : (i + 1) * chunkSize;
List<Employee> chunk = allEmployees.subList(fromIndex, toIndex);
threads[i] = new SalaryThread(chunk, i + 1);
threads[i].start(); // ★ 注意:调用 start() 而不是 run()
}
// 4. join() — 等待所有线程完成
for (int i = 0; i < threadCount; i++) {
threads[i].join(); // 主线程阻塞,等待子线程 i 完成
}
long totalEnd = System.currentTimeMillis();
// 5. 汇总结果
double grandTotal = 0;
for (Employee emp : allEmployees) {
grandTotal += emp.total;
}
System.out.println("\n========== 汇总结果 ==========");
System.out.printf("应发工资总额: %.2f 元\n", grandTotal);
System.out.printf("总耗时: %d ms\n", (totalEnd - totalStart));
// 打印前 5 个员工明细
System.out.println("\n--- 部分员工工资明细(前5名) ---");
for (int i = 0; i < 5; i++) {
System.out.println(allEmployees.get(i));
}
}
}
控制台输出(示例):
========== 飞翔科技薪酬计算系统 (多线程版) ==========
总员工数: 200
[线程-1] 开始处理 40 名员工的工资...
[线程-2] 开始处理 40 名员工的工资...
[线程-3] 开始处理 40 名员工的工资...
[线程-4] 开始处理 40 名员工的工资...
[线程-5] 开始处理 40 名员工的工资...
[线程-4] 处理完成,耗时: 48 ms
[线程-3] 处理完成,耗时: 50 ms
[线程-1] 处理完成,耗时: 52 ms
[线程-5] 处理完成,耗时: 53 ms
[线程-2] 处理完成,耗时: 55 ms
========== 汇总结果 ==========
应发工资总额: 2918456.32 元
总耗时: 56 ms
--- 部分员工工资明细(前5名) ---
员工[1] 张伟1: 基本=12000, 绩效=15000, 加班=2300, 合计=29300
员工[2] 李娜2: 基本=8000, 绩效=7200, 加班=1200, 合计=16400
...
示例 2:飞翔科技多客服并发处理用户请求 + interrupt 优雅停止
场景:飞翔科技客服系统,多个客服线程并行处理用户工单。下班时间到了,大翔需要通过 interrupt() 优雅地停止所有客服线程。
import java.util.concurrent.atomic.AtomicInteger;
/**
* 飞翔科技客服工单处理系统 — 多线程 + interrupt 优雅停止
*/
public class CustomerServiceSystem {
// 工单计数器(线程安全)
static final AtomicInteger ticketCounter = new AtomicInteger(0);
// 全局运行标志
static volatile boolean systemRunning = true;
// 客服线程 — 实现 Runnable(推荐方式)
static class CustomerServiceAgent implements Runnable {
private final String agentName;
public CustomerServiceAgent(String agentName) {
this.agentName = agentName;
}
@Override
public void run() {
System.out.printf("[%s] 上线,开始处理工单...\n", agentName);
while (systemRunning && !Thread.currentThread().isInterrupted()) {
// 取出一个工单
int ticketId = ticketCounter.incrementAndGet();
System.out.printf("[%s] 正在处理工单 #%d\n", agentName, ticketId);
// 模拟处理耗时
try {
Thread.sleep(500); // 模拟处理 500ms
} catch (InterruptedException e) {
// ★ interrupt 捕获:sleep 被中断
System.out.printf("[%s] 收到中断信号(工单 #%d 处理中),准备下线...\n",
agentName, ticketId);
// 恢复中断标志(因为 catch 会清除标志)
Thread.currentThread().interrupt();
break;
}
System.out.printf("[%s] 工单 #%d 处理完成 ✓\n", agentName, ticketId);
}
System.out.printf("[%s] 已下线,共处理工单。\n", agentName);
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("========== 飞翔科技客服工单系统 ==========\n");
// 创建 3 个客服线程
String[] agentNames = {"大翔", "白歌", "小崔"};
Thread[] agents = new Thread[agentNames.length];
for (int i = 0; i < agentNames.length; i++) {
agents[i] = new Thread(
new CustomerServiceAgent(agentNames[i]),
"客服-" + agentNames[i]
);
agents[i].start();
}
// 主线程让系统运行 3 秒,然后发起中断
System.out.println("[系统] 客服系统运行中...\n");
Thread.sleep(3000);
System.out.println("\n[系统] 下班时间到!通知所有客服下线...\n");
// ★ 第一步:设置全局标志
systemRunning = false;
// ★ 第二步:中断所有客服线程(处理那些正在 sleep 的线程)
for (Thread agent : agents) {
agent.interrupt();
}
// ★ 第三步:等待所有客服线程完成
for (Thread agent : agents) {
agent.join(2000); // 最多等 2 秒
}
System.out.println("\n[系统] 所有客服已下线。今天处理了 "
+ ticketCounter.get() + " 个工单。");
}
}
控制台输出(示例):
========== 飞翔科技客服工单系统 ==========
[大翔] 上线,开始处理工单...
[大翔] 正在处理工单 #1
[白歌] 上线,开始处理工单...
[白歌] 正在处理工单 #2
[小崔] 上线,开始处理工单...
[小崔] 正在处理工单 #3
[系统] 客服系统运行中...
[大翔] 工单 #1 处理完成 ✓
[大翔] 正在处理工单 #4
[白歌] 工单 #2 处理完成 ✓
[白歌] 正在处理工单 #5
[小崔] 工单 #3 处理完成 ✓
[小崔] 正在处理工单 #6
...
[系统] 下班时间到!通知所有客服下线...
[小崔] 收到中断信号(工单 #6 处理中),准备下线...
[大翔] 收到中断信号(工单 #4 处理中),准备下线...
[白歌] 收到中断信号(工单 #5 处理中),准备下线...
[小崔] 已下线
[大翔] 已下线
[白歌] 已下线
[系统] 所有客服已下线。今天处理了 18 个工单。
五、易错场景
易错 1:直接调用 run() 而非 start()
// ✗ 错误:直接调用 run() — 不会创建新线程!
Thread t = new Thread(() -> System.out.println("新线程"));
t.run(); // 在 main 线程执行,输出 "新线程",但没有并发
System.out.println("main 继续"); // 会在 run() 执行完才输出
// ✓ 正确:调用 start() — 创建新线程
Thread t2 = new Thread(() -> System.out.println("新线程"));
t2.start(); // 新线程异步执行
System.out.println("main 继续"); // 可能先于或后于 "新线程" 输出 — 这才是并发
后果:run() 直接调用不会创建新线程,代码在主线程中顺序执行,丧失了多线程的意义。在飞翔科技的薪酬计算系统中,如果小崔调用了 run() 而不是 start(),所有 1000 名员工的工资将在主线程中串行计算,耗时和单线程一样。
易错 2:join() 使用不当导致死等
// ✗ 错误:线程自己 join 自己 — 永远等待
Thread t = new Thread(() -> {
try {
Thread.currentThread().join(); // ★ 线程等待自身结束 → 死锁!
} catch (InterruptedException e) { }
});
t.start();
// ✗ 错误:join() 顺序不当 — 两个线程互相等待
Thread t1 = new Thread(() -> {
try { t2.join(); } catch (InterruptedException e) { } // t1 等 t2
});
Thread t2 = new Thread(() -> {
try { t1.join(); } catch (InterruptedException e) { } // t2 等 t1
});
t1.start(); t2.start();
// t1 等 t2 结束,t2 等 t1 结束 → 死锁!
// ✓ 正确:join() 用于父线程等待子线程
Thread worker = new Thread(() -> { /* 耗时任务 */ });
worker.start();
worker.join(); // 父线程等待 worker 完成
System.out.println("worker 已完成");
易错 3:interrupt() 标志位被吞掉
// ✗ 错误:catch InterruptedException 后不恢复中断标志
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ★ 吞掉了中断!循环会继续执行
System.out.println("被中断了...但是我还继续跑"); // 没有 break,没有恢复标志
}
}
}
// ✓ 正确方式一:捕获后 break
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break; // 直接退出循环
}
}
}
// ✓ 正确方式二:恢复中断标志
public void run() {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ★ 恢复标志,下次循环判断退出
}
}
}
原因:当 InterruptedException 被抛出后,JVM 会清除线程的中断标志位。如果不恢复标志或手动 break,线程永远不会停止 —— 在飞翔科技的客服系统中,大翔下班后点击"停止",但客服线程仍在继续处理工单。
易错 4:sleep() 不释放锁
// ✗ 误解:以为 sleep() 会释放锁
synchronized (lock) {
Thread.sleep(5000); // ★ sleep 期间仍然持有 lock 的监视器锁!
// 其他线程在这 5 秒内无法进入该 synchronized 块
}
// ✓ 如果需要释放锁等待,使用 wait()
synchronized (lock) {
lock.wait(5000); // ★ wait 会释放锁,其他线程可以获取 lock
}
关键区别:sleep() 是 Thread 的静态方法,让线程休眠但不释放任何锁。wait() 是 Object 的方法,必须在 synchronized 块中调用,且调用后会释放锁。
六、面试考点
Q1:请描述 Java 线程的 6 种状态,以及状态之间如何转换?
答:
| 状态 | 含义 | 转换触发条件 |
|---|---|---|
| NEW | 线程对象创建但未 start | new Thread() → 调用 start() 后进入 RUNNABLE |
| RUNNABLE | 可运行(含就绪和运行中) | CPU 调度分配时间片;获取锁后从 BLOCKED 返回 |
| BLOCKED | 等待进入 synchronized | 未竞争到监视器锁;获取锁后回到 RUNNABLE |
| WAITING | 无限期等待通知 | wait()/join()/park() → notify()/notifyAll()/unpark() 后回到 RUNNABLE |
| TIMED_WAITING | 有限期等待 | sleep(ms)/wait(ms)/join(ms) → 超时或通知后回到 RUNNABLE |
| TERMINATED | 线程终止 | run() 方法执行完毕或异常退出,不可恢复 |
状态转换图(见上文 Mermaid 图)。特别注意:
yield()不会改变线程状态,线程保持在 RUNNABLEBLOCKED特指synchronized锁竞争,Lock接口导致的是 WAITING- 从 WAITING/TIMED_WAITING 回到 RUNNABLE 后,仍需重新竞争 CPU 时间片
Q2:start() 和 run() 有什么区别?
答:
start():由 JVM 调用本地方法创建一个新的操作系统线程,然后在新线程中回调run()方法。这是实现多线程并发执行的唯一正确入口。run():仅是一个普通 Java 方法。直接调用run()不会创建新线程,代码在当前线程(如 main 线程)中顺序执行。- 每个 Thread 对象的
start()只能调用一次,重复调用会抛出IllegalThreadStateException。 - 底层原理:
start()→native start0()→ OS 创建线程 → JVM 回调run()。
Q3:sleep() 和 wait() 的核心区别是什么?
答:
| 维度 | sleep() | wait() |
|---|---|---|
| 所属类 | java.lang.Thread 静态方法 | java.lang.Object 实例方法 |
| 锁释放 | 不释放任何锁 | 释放当前对象监视器锁 |
| 调用条件 | 任何地方都可以调用 | 必须在 synchronized 块/方法中调用 |
| 唤醒方式 | 超时自动唤醒或 interrupt() | notify()/notifyAll() 或超时(带参数版本) |
| 用途 | 暂停执行,不涉及线程通信 | 线程间通信,等待条件满足 |
Q4:interrupt()、interrupted()、isInterrupted() 的区别?
答:
// interrupt() — 实例方法,设置目标线程的中断标志位
thread.interrupt(); // 如果线程在 sleep/wait/join 中,会抛出 InterruptedException
// interrupted() — 静态方法,检测当前线程的中断状态并清除标志位
boolean wasInterrupted = Thread.interrupted(); // ★ 返回后标志位被清除
// isInterrupted() — 实例方法,检测目标线程的中断状态,不清除标志位
boolean isInterrupted = thread.isInterrupted(); // ★ 标志位不变
关键陷阱:InterruptedException 被捕获后,JVM 会清除中断标志位。如果需要在捕获后保留中断状态,必须手动恢复:Thread.currentThread().interrupt();
七、JDK 8 关键 API 速查
// 线程创建方式 1:继承 Thread
class MyThread extends Thread {
@Override public void run() { /* 任务 */ }
}
new MyThread().start();
// 线程创建方式 2:实现 Runnable(推荐)
new Thread(() -> { /* 任务 */ }, "线程名").start();
// 线程创建方式 3:Callable + FutureTask(有返回值)
FutureTask<String> task = new FutureTask<>(() -> "结果");
new Thread(task).start();
String result = task.get(); // 阻塞等待
// 线程基本操作
Thread.sleep(1000); // 当前线程休眠 1 秒
thread.join(); // 等待 thread 线程终止
thread.join(5000); // 最多等 5 秒
Thread.yield(); // 提示调度器让出 CPU
thread.interrupt(); // 中断目标线程
thread.setDaemon(true); // 设为守护线程(必须在 start 前)
thread.setPriority(Thread.MAX_PRIORITY); // 设置优先级 1~10
// 线程状态查询
Thread.State state = thread.getState();
thread.isAlive(); // 线程是否存活
thread.isInterrupted(); // 是否被中断(不清除标志)
Thread.interrupted(); // 当前线程是否被中断(清除标志)
// UncaughtExceptionHandler — 处理未捕获异常
thread.setUncaughtExceptionHandler((t, e) -> {
System.err.println("线程 " + t.getName() + " 异常: " + e.getMessage());
});
后记:大翔用了三天时间把多线程基础吃透,薪资计算系统从 2 小时优化到了 8 分钟。小崔在旁边嘀咕:"原来 start() 和 run() 真的不一样啊……"
白歌:"这还只是开始。线程安全、锁机制、线程池——那才是真正的水深区。"
朱璐(微笑):"线程池的重要性,你们以后就懂了。现在先把 lifecycle 和 interrupt 弄明白吧。"