JAVA重点基础、进阶知识及易错点总结(16)多线程基础(Thread Runnable)
Java 巩固进阶 · 第16天主题多线程基础Thread Runnable—— 并发编程的起点 进度概览从今天起我们正式进入Java 进阶的分水岭多线程与并发编程。这是大厂面试必问、高性能系统必备的核心技能。 核心价值性能突破利用多核 CPU 并行处理让程序吞吐量提升 N 倍文件批量处理、接口并发调用。框架基石理解 Tomcat 线程池、SpringBootAsync、Redis 客户端连接池的底层原理。面试通关线程创建方式、start() vs run()、生命周期是初级→中级开发的必考题型。思维升级从顺序执行到并发思维为学习锁、线程池、分布式并发打下基础。一、核心概念程序·进程·线程 一图看懂 ️┌─────────────────────────────────────────┐ │ 程序 (Program) │ │ 静态的 .class/.jar 文件躺在硬盘里 │ │ 食谱 │ └─────────────────────────────────────────┘ ↓ 执行 ┌─────────────────────────────────────────┐ │ 进程 (Process) │ │ 运行中的程序实例如java -jar app.jar│ │ - 独立的内存空间堆/方法区 │ │ - 系统资源分配的基本单位 │ │ 正在做饭的厨房 │ └─────────────────────────────────────────┘ ↓ 内部 ┌─────────────────────────────────────────┐ │ 线程 (Thread) │ │ 进程内的执行单元轻量级 │ │ - 共享进程的堆内存独享栈内存 │ │ - CPU 调度的基本单位 │ │ 厨房里的厨师 可多个并行工作 │ └─────────────────────────────────────────┘ 为什么需要多线程场景单线程痛点多线程优势文件批量处理100 个文件串行处理耗时 100s10 线程并行理论耗时 10s ⚡Web 服务器一次只能处理 1 个请求用户排队Tomcat 线程池同时响应百级请求GUI 应用执行耗时任务时界面卡死后台线程计算主线程保持响应微服务调用串行调用 3 个接口延迟累加并行调用取最慢接口的耗时并发 (Concurrency) vs 并行 (Parallelism)并发单核 CPU 通过时间片轮转看起来同时执行宏观并行微观串行并行多核 CPU 真正同时执行多个任务需要硬件支持✅ 多线程既能实现并发也能利用多核实现并行二、创建线程的 2 种基础方式附选型指南方式1继承Thread类简单但受限/** * 方式1继承 Thread 类 * ⚠️ 缺点Java 单继承继承 Thread 后无法继承其他业务类 */classMyThreadextendsThread{// 业务参数通过构造注入privatefinalStringtaskName;publicMyThread(StringtaskName){this.taskNametaskName;}Overridepublicvoidrun(){// ✅ 线程执行的入口方法所有逻辑写在这里for(inti1;i5;i){System.out.println([taskName-getName()] 执行第 i 次);try{Thread.sleep(200);// 模拟耗时操作}catch(InterruptedExceptione){Thread.currentThread().interrupt();// ✅ 恢复中断状态最佳实践break;}}}}// 启动线程publicstaticvoidmain(String[]args){MyThreadt1newMyThread(任务A);MyThreadt2newMyThread(任务B);t1.start();// ✅ 启动新线程系统调度执行 run()t2.start();// ⚠️ 主线程继续执行不等待 t1/t2 完成System.out.println(主线程结束);}方式2实现Runnable接口⭐ 推荐/** * 方式2实现 Runnable 接口解耦 灵活 * ✅ 优点 * 1. 避免单继承限制类可继承其他业务父类 * 2. 任务Runnable与线程Thread分离符合单一职责 * 3. 天然兼容线程池execute(Runnable) */classMyTaskimplementsRunnable{privatefinalStringtaskName;publicMyTask(StringtaskName){this.taskNametaskName;}Overridepublicvoidrun(){// ✅ 获取当前执行线程的引用重要ThreadcurrentThread.currentThread();for(inti1;i5;i){System.out.println([taskName-current.getName()] 第 i 次);try{Thread.sleep(200);}catch(InterruptedExceptione){// ✅ 中断处理恢复中断标志 优雅退出current.interrupt();// 或 Thread.currentThread().interrupt()System.out.println([taskName] 被中断优雅退出);return;// 或 break}}System.out.println([taskName] 执行完成 ✨);}}// 启动线程publicstaticvoidmain(String[]args){// ✅ 同一个 Runnable 实例可被多个 Thread 共享注意线程安全MyTasktasknewMyTask(共享任务);Threadt1newThread(task,线程-1);// 第二个参数自定义线程名日志排查必备Threadt2newThread(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// Tomcat 请求处理每个请求封装为 Runnable交由线程池执行executor.execute(newRequestProcessor(request,response));// SpringBoot Async 底层将方法调用包装为 Runnable提交到任务执行器taskExecutor.execute(()-asyncMethod());三、致命陷阱start()vsrun()⚠️面试高频MyTasktasknewMyTask(测试);ThreadtnewThread(task);// ✅ 正确启动新线程run() 在新线程中执行t.start();// 输出[测试-线程-1] 第 1 次 线程名不是 main// ❌ 错误普通方法调用run() 在当前线程main中同步执行t.run();// 输出[测试-线程-1] 第 1 次 但线程名是 main// ❌ 严重重复 start() 会抛出 IllegalThreadStateExceptiont.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 速查表开发必备// 获取线程引用ThreadcurrentThread.currentThread();// 当前执行线程ThreadmainThread.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(InterruptedExceptione){// ✅ 中断处理恢复中断状态重要Thread.currentThread().interrupt();}// ⏳ 等待线程结束主线程等待子线程ThreadworkernewThread(()-{// 耗时任务...});worker.start();worker.join();// 主线程阻塞直到 worker 执行完毕// ✅ 重载join(1000) 最多等待 1 秒避免无限阻塞// 线程礼让提示调度器但不保证Thread.yield();// 我愿意让出 CPU有其他线程就让他们先执行// ⚡ 线程优先级1~10默认 5不保证执行顺序worker.setPriority(Thread.MAX_PRIORITY);// 10 最高优先级// ⚠️ 注意优先级依赖操作系统Java 不保证高优先级一定先执行// ️ 守护线程随主线程结束而自动终止适合后台任务ThreaddaemonnewThread(()-{while(true){// 监控/清理等后台任务Thread.sleep(60000);}});daemon.setDaemon(true);// ⚠️ 必须在 start() 前设置daemon.start();中断机制最佳实践// ❌ 错误吞掉中断信号导致线程无法被优雅停止try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();// 中断标志被清除}// ✅ 正确恢复中断标志让上层调用者感知try{Thread.sleep(1000);}catch(InterruptedExceptione){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 → Runnablestart()new Thread(r).start()Runnable → RunningCPU 调度操作系统决定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 → Terminatedrun()执行完毕正常返回或抛出未捕获异常调试技巧打印线程状态ThreadtnewThread(()-{System.out.println(线程状态: Thread.currentThread().getState());// NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED});六、 今日实战任务多线程文件处理器任务1实现并行打印任务/** * 要求 * 1. 实现 Runnable循环打印 1~10每次打印后 sleep(100ms) * 2. 主线程也循环打印 1~10不 sleep * 3. 启动 2 个子线程 主线程观察输出交替效果 * * 提示 * - 为每个线程设置语义化名称 Printer-1, Printer-2, Main * - 观察输出顺序的不确定性并发核心特征 */任务2实现文件统计多线程版综合练习/** * 统计多个文本文件的行数模拟日志分析场景 * * 要求 * 1. 创建 FileCounter implements Runnable统计单个文件行数 * 2. 主线程创建 3 个 FileCounter 线程分别处理 file1.txt, file2.txt, file3.txt * 3. 用 join() 等待所有线程完成再输出总行数 * * 挑战 * - 如何处理文件不存在/读取异常 * - 如何汇总各线程的统计结果提示用 AtomicInteger 或volatile明天学 */publicclassFileCounterimplementsRunnable{privatefinalFilefile;privateintlineCount0;// ⚠️ 注意这个字段线程安全吗publicFileCounter(Filefile){this.filefile;}Overridepublicvoidrun(){// TODO: 用 BufferedReader 按行读取统计行数// 注意捕获 IOException 中断处理}publicintgetLineCount(){returnlineCount;}}任务3模拟用户注册异步流程SpringBoot 前置/** * 用户注册主流程 异步发送欢迎邮件 * * 要求 * 1. 主线程模拟保存用户到数据库sleep 500ms * 2. 启动新线程模拟发送邮件sleep 1000ms * 3. 主线程不等待邮件发送完成直接返回注册成功 * * 思考 * - 如果邮件发送失败如何记录日志提示线程内 try-catch * - 如何确保应用关闭时未完成的邮件任务能被优雅终止守护线程/中断 */任务4线程命名规范实践生产环境必备/** * 为不同业务场景的线程设置语义化名称便于日志排查 * * 要求 * 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() 前设置中断处理最佳实践try{Thread.sleep(1000);}catch(InterruptedExceptione){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()