🚀 Java 巩固进阶 · 第16天
主题:多线程基础(Thread & Runnable)------ 并发编程的起点
📅 进度概览 :从今天起,我们正式进入 Java 进阶的分水岭:多线程与并发编程。这是大厂面试必问、高性能系统必备的核心技能。
💡 核心价值:
- 性能突破:利用多核 CPU 并行处理,让程序吞吐量提升 N 倍(文件批量处理、接口并发调用)。
- 框架基石 :理解 Tomcat 线程池、SpringBoot
@Async、Redis 客户端连接池的底层原理。- 面试通关:线程创建方式、start() vs run()、生命周期,是初级→中级开发的必考题型。
- 思维升级:从"顺序执行"到"并发思维",为学习锁、线程池、分布式并发打下基础。
一、核心概念:程序·进程·线程 一图看懂 🗺️
┌─────────────────────────────────────────┐
│ 📦 程序 (Program) │
│ 静态的 .class/.jar 文件,躺在硬盘里 │
│ "食谱" 📝 │
└─────────────────────────────────────────┘
↓ 执行
┌─────────────────────────────────────────┐
│ 🔄 进程 (Process) │
│ 运行中的程序实例,如:java -jar app.jar│
│ - 独立的内存空间(堆/方法区) │
│ - 系统资源分配的基本单位 │
│ "正在做饭的厨房" 🍳 │
└─────────────────────────────────────────┘
↓ 内部
┌─────────────────────────────────────────┐
│ 🧵 线程 (Thread) │
│ 进程内的执行单元,轻量级 │
│ - 共享进程的堆内存,独享栈内存 │
│ - CPU 调度的基本单位 │
│ "厨房里的厨师" 👨🍳👩🍳(可多个并行工作) │
└─────────────────────────────────────────┘
🔍 为什么需要多线程?
| 场景 | 单线程痛点 | 多线程优势 |
|---|---|---|
| 文件批量处理 | 100 个文件串行处理,耗时 100s | 10 线程并行,理论耗时 10s ⚡ |
| Web 服务器 | 一次只能处理 1 个请求,用户排队 | Tomcat 线程池,同时响应百级请求 |
| GUI 应用 | 执行耗时任务时界面卡死 | 后台线程计算,主线程保持响应 |
| 微服务调用 | 串行调用 3 个接口,延迟累加 | 并行调用,取最慢接口的耗时 |
💡 并发 (Concurrency) vs 并行 (Parallelism):
- 并发:单核 CPU 通过时间片轮转,"看起来"同时执行(宏观并行,微观串行)
- 并行:多核 CPU 真正同时执行多个任务(需要硬件支持)
- ✅ 多线程既能实现并发,也能利用多核实现并行
二、创建线程的 2 种基础方式(附选型指南)
方式1:继承 Thread 类(简单但受限)
java
/**
* 方式1:继承 Thread 类
* ⚠️ 缺点:Java 单继承,继承 Thread 后无法继承其他业务类
*/
class MyThread extends Thread {
// 业务参数(通过构造注入)
private final String taskName;
public MyThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
// ✅ 线程执行的入口方法(所有逻辑写在这里)
for (int i = 1; i <= 5; i++) {
System.out.println("[" + taskName + "-" + getName() + "] 执行第 " + i + " 次");
try {
Thread.sleep(200); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // ✅ 恢复中断状态(最佳实践)
break;
}
}
}
}
// 启动线程
public static void main(String[] args) {
MyThread t1 = new MyThread("任务A");
MyThread t2 = new MyThread("任务B");
t1.start(); // ✅ 启动新线程(系统调度执行 run())
t2.start();
// ⚠️ 主线程继续执行,不等待 t1/t2 完成
System.out.println("主线程结束");
}
方式2:实现 Runnable 接口(⭐ 推荐!)
java
/**
* 方式2:实现 Runnable 接口(解耦 + 灵活)
* ✅ 优点:
* 1. 避免单继承限制,类可继承其他业务父类
* 2. 任务(Runnable)与线程(Thread)分离,符合单一职责
* 3. 天然兼容线程池(execute(Runnable))
*/
class MyTask implements Runnable {
private final String taskName;
public MyTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
// ✅ 获取当前执行线程的引用(重要!)
Thread current = Thread.currentThread();
for (int i = 1; i <= 5; i++) {
System.out.println("[" + taskName + "-" + current.getName() + "] 第 " + i + " 次");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// ✅ 中断处理:恢复中断标志 + 优雅退出
current.interrupt(); // 或 Thread.currentThread().interrupt()
System.out.println("[" + taskName + "] 被中断,优雅退出");
return; // 或 break
}
}
System.out.println("[" + taskName + "] 执行完成 ✨");
}
}
// 启动线程
public static void main(String[] args) {
// ✅ 同一个 Runnable 实例可被多个 Thread 共享(注意线程安全!)
MyTask task = new MyTask("共享任务");
Thread t1 = new Thread(task, "线程-1"); // 第二个参数:自定义线程名(日志排查必备!)
Thread t2 = new Thread(task, "线程-2");
t1.start();
t2.start();
// 💡 进阶:为线程设置未捕获异常处理器(生产环境推荐)
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
System.err.println("线程 [" + t.getName() + "] 发生未捕获异常: " + e.getMessage());
// 可集成日志框架:log.error("Uncaught exception in thread {}", t.getName(), e);
});
}
🔍 两种方式对比 & 选型建议
| 对比项 | 继承 Thread | 实现 Runnable(推荐) |
|---|---|---|
| 继承限制 | ❌ 占用唯一继承名额 | ✅ 可继承其他业务类 |
| 任务复用 | ❌ 每个线程独立实例 | ✅ 同一任务可被多线程共享 |
| 资源开销 | 略高(每个线程独立对象) | 略低(任务对象可复用) |
| 线程池兼容 | ❌ 需额外包装 | ✅ 天然支持 execute(runnable) |
| 适用场景 | 简单脚本、学习演示 | 生产环境、框架开发 |
💡 为什么 SpringBoot/Tomcat 都用 Runnable?
java// Tomcat 请求处理:每个请求封装为 Runnable,交由线程池执行 executor.execute(new RequestProcessor(request, response)); // SpringBoot @Async 底层:将方法调用包装为 Runnable,提交到任务执行器 taskExecutor.execute(() -> asyncMethod());
三、致命陷阱:start() vs run() ⚠️(面试高频)
java
MyTask task = new MyTask("测试");
Thread t = new Thread(task);
// ✅ 正确:启动新线程,run() 在新线程中执行
t.start();
// 输出:[测试-线程-1] 第 1 次 (线程名不是 main!)
// ❌ 错误:普通方法调用,run() 在当前线程(main)中同步执行
t.run();
// 输出:[测试-线程-1] 第 1 次 (但线程名是 main!)
// ❌ 严重:重复 start() 会抛出 IllegalThreadStateException
t.start(); // 第一次 ✅
t.start(); // 第二次 ❌ Exception in thread "main" java.lang.IllegalThreadStateException
🔍 底层原理图解
调用 t.start() 时:
1. JVM 向操作系统申请创建新线程
2. 新线程就绪,等待 CPU 调度
3. 调度到该线程时,自动调用其 run() 方法
4. run() 执行完毕,线程终止
调用 t.run() 时:
1. 就是普通方法调用,类似 t.toString()
2. 代码在当前线程同步执行,无并发效果
💡 记忆口诀:
start()= "启动" → 开新线程 → 异步执行run()= "运行" → 普通方法 → 同步执行- ✅ 永远用 start() 启动线程!
四、线程常用 API 速查表(开发必备)
java
// 🎯 获取线程引用
Thread current = Thread.currentThread(); // 当前执行线程
Thread main = Thread.getAllStackTraces().keySet()
.stream()
.filter(t -> t.getName().equals("main"))
.findFirst().orElse(null);
// 🏷️ 线程命名(日志排查关键!)
current.setName("Order-Process-Thread"); // 业务语义化命名
System.out.println(current.getName()); // 输出: Order-Process-Thread
// 😴 线程休眠(模拟耗时/限流)
try {
Thread.sleep(1000); // 休眠 1 秒(毫秒)
// ⚠️ sleep() 不释放锁!如果持有 synchronized 锁,其他线程仍无法进入
} catch (InterruptedException e) {
// ✅ 中断处理:恢复中断状态(重要!)
Thread.currentThread().interrupt();
}
// ⏳ 等待线程结束(主线程等待子线程)
Thread worker = new Thread(() -> {
// 耗时任务...
});
worker.start();
worker.join(); // 主线程阻塞,直到 worker 执行完毕
// ✅ 重载:join(1000) 最多等待 1 秒,避免无限阻塞
// 🤝 线程礼让(提示调度器,但不保证)
Thread.yield(); // "我愿意让出 CPU,有其他线程就让他们先执行"
// ⚡ 线程优先级(1~10,默认 5,不保证执行顺序!)
worker.setPriority(Thread.MAX_PRIORITY); // 10 = 最高优先级
// ⚠️ 注意:优先级依赖操作系统,Java 不保证高优先级一定先执行
// 🛡️ 守护线程(随主线程结束而自动终止,适合后台任务)
Thread daemon = new Thread(() -> {
while (true) {
// 监控/清理等后台任务
Thread.sleep(60000);
}
});
daemon.setDaemon(true); // ⚠️ 必须在 start() 前设置!
daemon.start();
💡 中断机制最佳实践:
java// ❌ 错误:吞掉中断信号,导致线程无法被优雅停止 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); // 中断标志被清除! } // ✅ 正确:恢复中断标志,让上层调用者感知 try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 // 可选:记录日志 + 清理资源 + 退出 log.warn("线程被中断,正在清理..."); return; }
五、线程生命周期(5 状态模型)🔄
┌─────────────┐
│ 新建 (New) │
│ new Thread()│
└──────┬──────┘
↓ start()
┌─────────────┐
│ 就绪 (Runnable)│
│ 等待 CPU 调度 │
└──────┬──────┘
↓ 获得 CPU
┌─────────────┐
│ 运行 (Running)│
│ 执行 run() │
└──────┬──────┘
↓ 遇到阻塞
┌─────────────────┴─────────────────┐
↓ ↓ ↓
┌───────────┐ ┌─────────────┐ ┌─────────────┐
│ 阻塞 (Blocked)│ │ 等待 (Waiting)│ │ 超时等待 │
│ 等待锁 │ │ wait()/join()│ │ sleep(1000)│
└─────┬─────┘ └─────┬───────┘ └─────┬───────┘
│ │ │
└─────┬───────┴────────────────┘
↓ 获得资源/被通知/时间到
┌─────────────┐
│ 就绪 (Runnable)│ ←── 循环
└──────┬──────┘
↓ run() 结束 / 异常退出
┌─────────────┐
│ 终止 (Terminated)│
│ 线程销毁 │
└─────────────┘
🔍 关键状态转换触发条件
| 状态转换 | 触发条件 | 代码示例 |
|---|---|---|
| New → Runnable | start() |
new Thread(r).start() |
| Runnable → Running | CPU 调度 | 操作系统决定 |
| Running → Blocked | 等待 synchronized 锁 | synchronized(obj) { ... } |
| Running → Waiting | 无超时等待 | obj.wait(), thread.join() |
| Running → Timed Waiting | 带超时等待 | Thread.sleep(1000), obj.wait(1000) |
| Blocked/Waiting → Runnable | 获得锁 / 被通知 / 超时 | notify(), 锁释放, 时间到 |
| Running → Terminated | run() 执行完毕 |
正常返回或抛出未捕获异常 |
💡 调试技巧:打印线程状态
javaThread t = new Thread(() -> { System.out.println("线程状态: " + Thread.currentThread().getState()); // NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED });
六、🎯 今日实战任务:多线程文件处理器
任务1:实现"并行打印"任务
java
/**
* 要求:
* 1. 实现 Runnable,循环打印 1~10,每次打印后 sleep(100ms)
* 2. 主线程也循环打印 1~10(不 sleep)
* 3. 启动 2 个子线程 + 主线程,观察输出交替效果
*
* 💡 提示:
* - 为每个线程设置语义化名称: "Printer-1", "Printer-2", "Main"
* - 观察输出顺序的"不确定性"(并发核心特征!)
*/
任务2:实现"文件统计"多线程版(综合练习)
java
/**
* 统计多个文本文件的行数(模拟日志分析场景)
*
* 要求:
* 1. 创建 FileCounter implements Runnable,统计单个文件行数
* 2. 主线程创建 3 个 FileCounter 线程,分别处理 file1.txt, file2.txt, file3.txt
* 3. 用 join() 等待所有线程完成,再输出总行数
*
* 💡 挑战:
* - 如何处理文件不存在/读取异常?
* - 如何汇总各线程的统计结果?(提示:用 AtomicInteger 或volatile,明天学)
*/
public class FileCounter implements Runnable {
private final File file;
private int lineCount = 0; // ⚠️ 注意:这个字段线程安全吗?
public FileCounter(File file) { this.file = file; }
@Override
public void run() {
// TODO: 用 BufferedReader 按行读取,统计行数
// 注意:捕获 IOException + 中断处理
}
public int getLineCount() { return lineCount; }
}
任务3:模拟"用户注册"异步流程(SpringBoot 前置)
java
/**
* 用户注册:主流程 + 异步发送欢迎邮件
*
* 要求:
* 1. 主线程:模拟保存用户到数据库(sleep 500ms)
* 2. 启动新线程:模拟发送邮件(sleep 1000ms)
* 3. 主线程不等待邮件发送完成,直接返回"注册成功"
*
* 💡 思考:
* - 如果邮件发送失败,如何记录日志?(提示:线程内 try-catch)
* - 如何确保应用关闭时,未完成的邮件任务能被优雅终止?(守护线程/中断)
*/
任务4:线程命名规范实践(生产环境必备)
java
/**
* 为不同业务场景的线程设置语义化名称,便于日志排查
*
* 要求:
* 1. 文件处理线程: "FileProcessor-{taskId}"
* 2. 邮件发送线程: "EmailSender-{userId}"
* 3. 定时任务线程: "Scheduler-CleanLog-{cron}"
*
* 💡 最佳实践:
* - 线程名 = 业务模块 + 任务标识 + 唯一序号(可选)
* - 避免默认名称 "Thread-0", "Thread-1"(出问题难以定位)
*/
📝 第16天 · 核心总结(极简背诵版)
-
线程创建选型:
优先实现 Runnable → 避免单继承限制 + 兼容线程池 继承 Thread → 仅用于简单脚本/学习 -
启动铁律:
- ✅
start():启动新线程,异步执行run() - ❌
run():普通方法调用,同步执行,无并发效果 - ❌ 重复
start():抛出IllegalThreadStateException
- ✅
-
关键 API 速记:
Thread.currentThread():获取当前线程引用(日志/中断必备)sleep(ms):线程休眠,不释放锁 ,必须处理InterruptedExceptionjoin():等待线程结束,主线程阻塞setDaemon(true):守护线程,随主线程自动终止(必须在 start() 前设置)
-
中断处理最佳实践:
javatry { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // ✅ 恢复中断标志 // 可选:清理资源 + 记录日志 + 优雅退出 return; } -
线程命名规范(生产环境红线!):
- ✅ 语义化:
"Order-Process-User123","Email-Sender-Task456" - ❌ 默认名:
"Thread-0","Thread-1"(出问题无法定位)
- ✅ 语义化:
-
生命周期核心:
- 5 状态:New → Runnable → Running → (Blocked/Waiting) → Terminated
- 关键转换:
start()、sleep()、wait()/notify()、join()