JAVA重点基础、进阶知识及易错点总结(16)多线程基础(Thread & Runnable)

🚀 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() 执行完毕 正常返回或抛出未捕获异常

💡 调试技巧:打印线程状态

java 复制代码
Thread 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天 · 核心总结(极简背诵版)

  1. 线程创建选型

    复制代码
    优先实现 Runnable → 避免单继承限制 + 兼容线程池
    继承 Thread → 仅用于简单脚本/学习
  2. 启动铁律

    • start():启动新线程,异步执行 run()
    • run():普通方法调用,同步执行,无并发效果
    • ❌ 重复 start():抛出 IllegalThreadStateException
  3. 关键 API 速记

    • Thread.currentThread():获取当前线程引用(日志/中断必备)
    • sleep(ms):线程休眠,不释放锁 ,必须处理 InterruptedException
    • join():等待线程结束,主线程阻塞
    • setDaemon(true):守护线程,随主线程自动终止(必须在 start() 前设置
  4. 中断处理最佳实践

    java 复制代码
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();  // ✅ 恢复中断标志
        // 可选:清理资源 + 记录日志 + 优雅退出
        return;
    }
  5. 线程命名规范(生产环境红线!):

    • ✅ 语义化:"Order-Process-User123", "Email-Sender-Task456"
    • ❌ 默认名:"Thread-0", "Thread-1"(出问题无法定位)
  6. 生命周期核心

    • 5 状态:New → Runnable → Running → (Blocked/Waiting) → Terminated
    • 关键转换:start()sleep()wait()/notify()join()

相关推荐
misty youth2 小时前
提示词合集【自用】
开发语言·前端·ai编程
大数据新鸟2 小时前
java8基础知识--字符串
java
zero15972 小时前
Python 8天极速入门笔记(大模型工程师专用):第六篇-函数进阶 + 模块导入,大模型实战调用前置
开发语言·python·大模型编程语言
ChoSeitaku2 小时前
NO.2|数据结构设计|日志封装|DeepSeel接入封装|全量返回实现测试|SSE|流式响应实现测试
java·jvm·数据结构
还是大剑师兰特2 小时前
为什么要用 import.meta.glob 加载 SVG 图标库
开发语言·前端·javascript
谪星·阿凯2 小时前
PHP序列化漏洞从入门到实战博客
android·开发语言·web安全·php
wjs20242 小时前
Bootstrap4 输入框组
开发语言
斌味代码2 小时前
后端实战实战案例
java
小信丶2 小时前
彻底解决 IDEA 启动 SpringBoot 报错:Command line is too long
java·spring boot·intellij-idea