# 2026年8月10日 课后作业 — 线程操作方法、生命周期、守护线程与 synchronized 线程同步 > **说明**:本次课程学习了 **多线程的进阶内容**——**线程操作方法**(`Thread.sleep()` 线程休眠、`getPriority()` / `setPriority()` 线程优先级、抢占式调度模型与 CPU 时间片随机性、`join()` / `interrupt()` / `yield()` / `isAlive()`、Object 类 `wait()` / `notify()` / `notifyAll()`)、**线程生命周期**(`Thread.State` 枚举 6 种状态:NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED、`getState()` 观察状态)、**守护线程**(`setDaemon()` / `isDaemon()`、JVM 退出机制、守护线程特点)、**线程安全问题**(数据竞争、卖票问题、计数器问题、线程安全三大特性:原子性 / 可见性 / 有序性)以及 **synchronized 线程同步**(监视器锁 Monitor Lock、三种写法:同步实例方法锁 this / 同步静态方法锁 Class 对象 / 同步代码块锁指定对象、锁粒度优化、转账业务场景)。 > > 本作业共 **3 道题**:**基础题(必做)**——线程优先级 + 生命周期观察赛跑;**进阶题(必做)**——守护线程后台心跳监控(含 join 等待与状态观察);**挑战题(选做)**——synchronized 线程同步修复卖票问题。覆盖今日核心知识点。 --- ## 📌 作业总体要求 ### 一、命名要求 | 项目 | 要求 | |------|------| | **类名** | `Demo_题号_你的姓名`(如 `Demo_01_张三`、`Demo_02_李四`、`Demo_03_王五`) | | **文件头注释** | 每个 Java 文件头部需包含 `@author`、`@title`、`@create` 文档注释 | ### 二、提交方式与截止时间 | 项目 | 要求 | |------|------| | **提交方式** | 将源码文件 + 运行结果截图提交至班级指定平台 | | **截止时间** | 下次上课前 | --- ## 一、基础题(必做) ### 题目 1:线程优先级 + 生命周期观察赛跑 RaceLifecycleDemo(⭐⭐ 必做) **知识点**:`setPriority()` / `getPriority()`(默认 5、范围 1~10)、`Thread.MAX_PRIORITY` / `NORM_PRIORITY` / `MIN_PRIORITY`、`Thread.sleep()` 线程休眠、抢占式调度模型与多线程执行随机性、**线程生命周期**(`Thread.State` 枚举、`getState()` 观察 NEW / RUNNABLE / TIMED_WAITING / TERMINATED)、Lambda 创建 Runnable、`join()` 等待线程结束 **场景描述**:课堂用 `Demo01` 演示了 Lambda 创建 Runnable + `setPriority(8)` 设置优先级 + `sleep(100)` 让双线程并发交错;用 `Demo03` 的 `getState()` 观察线程生命周期状态。请综合实现:创建 **3 个"运动员"线程**(优先级分别为 10 / 5 / 1)赛跑 10 圈,每圈 `sleep(100)` 模拟跑步;同时用 `getState()` 观察**某个线程**在不同阶段的状态变化,体会优先级对抢占调度的影响和生命周期状态流转。 **题目要求**: ```java /** * @author 你的姓名 * @title 题目1:线程优先级 + 生命周期观察赛跑 * @create 2026/8/10 */ public class Demo_01_姓名 { // ========== ① Lambda 创建 Runnable 运动员任务 ========== static Runnable runner = () -> { // TODO 1: String name = Thread.currentThread().getName(); // 获取线程名 // TODO 2: int p = Thread.currentThread().getPriority(); // 获取优先级 // TODO 3: 循环 10 圈,每圈 sleep(100) 后打印 name + "(优先级p)跑完第 i 圈" // TODO 4: 循环结束打印 name + " 到达终点!" }; public static void main(String[] args) throws InterruptedException { // ========== ② 创建 3 个运动员线程 ========== // TODO 5: new Thread(runner, "运动员1") / "运动员2" / "运动员3" // TODO 6: 打印 3 个线程的默认优先级(预期都是 5) // ========== ③ 设置不同优先级 ========== // TODO 7: t1.setPriority(Thread.MAX_PRIORITY); // 10 // t2.setPriority(Thread.NORM_PRIORITY); // 5 // t3.setPriority(Thread.MIN_PRIORITY); // 1 // ========== ④ 启动线程前观察 NEW 状态 ========== // TODO 8: System.out.println("启动前状态:" + t1.getState()); // NEW // ========== ⑤ 启动 3 个线程 ========== // TODO 9: t1.start(); t2.start(); t3.start(); // ========== ⑥ 观察 t1 运行中的状态 ========== // TODO 10: 主线程 sleep(50) 后打印 t1.getState() // 可能是 RUNNABLE 或 TIMED_WAITING // TODO 11: 用 t1.join() 等待 t1 结束,再打印 t1.getState() // TERMINATED } } ``` **预期输出示例**(每次运行顺序可能不同): ``` 运动员1 默认优先级:5 运动员2 默认优先级:5 运动员3 默认优先级:5 启动前状态:NEW 运动员1(优先级10)跑完第 1 圈 运动员3(优先级1)跑完第 1 圈 运动员2(优先级5)跑完第 1 圈 ... 运行中状态:TIMED_WAITING ... 运动员1 到达终点! 结束后状态:TERMINATED ``` **要求说明**: 1. 必须用 Lambda 创建 Runnable 任务,3 个线程共用一个任务对象 2. 必须设置三种不同优先级(MAX=10 / NORM=5 / MIN=1),并打印验证 3. 必须用 `getState()` 至少观察 **NEW / TIMED_WAITING / TERMINATED** 三个状态(RUNNABLE 尽量观察) 4. 主线程最后必须用 `t1.join()` 等待 t1 结束 **💡 提示**: - `Thread.MAX_PRIORITY`=10、`NORM_PRIORITY`=5(默认)、`MIN_PRIORITY`=1,`setPriority()` 超范围抛 `IllegalArgumentException` - 优先级只是提高获得 CPU 的**概率**,不保证先执行完(抢占式调度 + 随机性) - `sleep(100)` 期间线程处于 **TIMED_WAITING**;`new Thread()` 未 start 是 **NEW**;run() 结束是 **TERMINATED** - `getState()` 返回 `Thread.State` 枚举:`NEW` / `RUNNABLE` / `BLOCKED` / `WAITING` / `TIMED_WAITING` / `TERMINATED` **🎯 挑战加分(选做)**: 1. 把 `setPriority(11)` 传入试试,观察抛出的 `IllegalArgumentException` 2. 把循环圈数改大(如 1000)并去掉 sleep,多运行几次,统计"到达终点"顺序,观察高优先级线程是否更可能先到 3. 尝试让两个线程竞争一个 `synchronized` 锁,观察未抢到锁线程的 `getState()` 是否为 **BLOCKED**(提前预习线程同步) --- ## 二、进阶题(必做) ### 题目 2:守护线程后台心跳监控 + join 等待 DaemonHeartbeatDemo(⭐⭐⭐ 必做) **知识点**:**守护线程**(`setDaemon(true)` / `isDaemon()`、JVM 退出机制:所有用户线程结束才退出、只剩守护线程时 JVM 强制终止守护线程)、守护线程特点(必须在 `start()` 前设置、被强杀时 finally 不保证执行)、**用户线程 vs 守护线程**对比、`join()` 等待线程结束、`sleep()` 线程休眠、线程生命周期状态观察 **场景描述**:课堂用 `Demo02` 演示了守护线程——"主任务(用户线程)" + "心跳监控(守护线程)",main 结束后 JVM 退出、守护线程被强制终止。请实现:**主任务线程**执行 5 秒业务(每秒打印一次进度),**心跳监控线程**(守护线程)每 500ms 报告一次心跳;主线程用 `join()` 等待主任务结束,观察 `main` 结束后心跳线程是否被 JVM 强制终止,并打印 `isDaemon()` 验证两个线程的守护属性。 **题目要求**: ```java /** * @author 你的姓名 * @title 题目2:守护线程后台心跳监控 + join 等待 * @create 2026/8/10 */ public class Demo_02_姓名 { public static void main(String[] args) throws InterruptedException { // ========== ① 用户线程(主任务):执行 5 秒 ========== Thread task = new Thread(() -> { for (int i = 1; i <= 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 重设中断标志 break; } // TODO 1: 打印 "主任务执行中...第 i 秒" } // TODO 2: 打印 "主任务执行完毕!" }, "主任务"); // ========== ② 守护线程(心跳监控):无限循环 ========== Thread heart = new Thread(() -> { int count = 0; while (true) { try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } // TODO 3: 打印 "心跳检测中...第 (++count) 次" } }, "心跳监控"); // ========== ③ 设置守护线程(必须在 start() 之前!)========== // TODO 4: heart.setDaemon(true); // ========== ④ 验证 isDaemon ========== // TODO 5: 打印 heart.isDaemon()(预期 true)和 task.isDaemon()(预期 false) // ========== ⑤ 启动两个线程 ========== // TODO 6: task.start(); heart.start(); // ========== ⑥ 主线程 join 等待主任务结束 ========== // TODO 7: task.join(); 主线程等待主任务执行完 // TODO 8: 打印 "main 结束,观察心跳线程是否还在打印" // main 结束后只剩守护线程 → JVM 退出 → 心跳线程被强制终止 } } ``` **预期输出示例**(每次运行顺序可能不同): ``` 心跳监控是否是守护线程:true 主任务是否是守护线程:false 主任务执行中...第 1 秒 心跳检测中...第 1 次 心跳检测中...第 2 次 心跳检测中...第 3 次 主任务执行中...第 2 秒 ... 主任务执行完毕! main 结束,观察心跳线程是否还在打印 ← main 结束,JVM 退出,心跳检测不再打印 ``` **要求说明**: 1. 心跳线程必须用 `setDaemon(true)` 标记为守护线程,且必须在 `start()` **之前**调用 2. 必须打印 `isDaemon()` 验证两个线程的守护属性(主任务 false、心跳 true) 3. 主线程必须用 `task.join()` 等待主任务执行结束 4. 运行后观察 main 结束后**心跳线程是否被强制终止**(不再打印) **💡 提示**: - **JVM 退出条件**:只有当所有用户线程(普通线程)都结束时 JVM 才退出;main 主线程也是用户线程 - **守护线程被强杀**:当只剩守护线程时 JVM 直接退出,守护线程被强制终止,`finally` 不保证执行(所以不要用守护线程做资源清理) - 把 `heart.setDaemon(true)` 移到 `heart.start()` 之后会抛 `IllegalThreadStateException` - 如果把心跳线程改成**用户线程**(不 setDaemon),main 结束后 JVM **不会退出**,心跳会一直打印(对比体会) **🎯 挑战加分(选做)**: 1. 把 `setDaemon(true)` 移到 `start()` 之后运行,观察抛出的 `IllegalThreadStateException` 2. 把心跳线程改成用户线程(不 setDaemon),运行后观察 main 结束后 JVM 是否退出(心跳一直打印停不下来) 3. 在心跳线程 run() 的 try-finally 中打印"资源清理",观察守护线程被强杀时 finally 是否执行(体会"finally 不保证") --- ## 三、挑战题(选做) ### 题目 3:synchronized 线程同步 —— 修复卖票问题 SellTicketSyncDemo(⭐⭐⭐⭐ 选做) **知识点**:**线程安全问题**(数据竞争、进程间不共享 vs 线程间共享)、**卖票问题**(相同票多次出现 / 负数票两个 Bug 及其原因:读-改-写被并发打断)、**线程安全三大特性**(原子性 / 可见性 / 有序性)、**synchronized 三种写法**(同步实例方法锁 this / 同步静态方法锁 Class 对象 / 同步代码块锁指定对象)、**监视器锁(Monitor Lock)**、锁粒度优化、`join()` 等待线程结束 **场景描述**:课堂 `Demo04` + `SellTicket` 演示了**卖票问题**——3 个售票窗口线程同时卖 100 张票,出现"相同的票多次出现"和"负数票"两个 Bug;`SynchronizedDemo` 用 synchronized 解决。请完成:**先写无 synchronized 的卖票代码复现 Bug**,再用 **synchronized 三种写法**(同步方法 / 同步代码块锁 this / 同步代码块锁自定义对象)修复,最后验证总票数正确、无重复、无负数。 **题目要求**: ```java /** * @author 你的姓名 * @title 题目3:synchronized 线程同步修复卖票问题 * @create 2026/8/10 */ public class Demo_03_姓名 { // ========== ① 卖票类:实现 Runnable ========== static class SellTicket implements Runnable { private int tickets = 100; // 一共 100 张票 // 写法1:同步方法(锁 this)—— TODO 2 完成后给方法加 synchronized public void sell() { // TODO 1: if (tickets > 0) { sleep(10); 打印 "xxx正在出售第 tickets 张票"; tickets--; } } @Override public void run() { while (tickets > 0) { sell(); // TODO 2: 先不加 synchronized 运行复现 Bug,再加 synchronized 修复 } } } public static void main(String[] args) throws InterruptedException { // ========== ② 创建 3 个售票窗口线程(共用同一个 SellTicket 对象) ========== // TODO 3: new Thread(new SellTicket(), "窗口1") / "窗口2" / "窗口3",start() 启动 // TODO 4: 用线程数组 + join() 等待所有线程卖票结束 // TODO 5: 运行多次观察:无 synchronized 时出现相同票 / 负数票;加 synchronized 后消失 } } ``` **预期输出示例**(无 synchronized 时——Bug 复现): ``` 窗口1正在出售第10张票 窗口2正在出售第10张票 ← Bug1:相同的票多次出现 窗口3正在出售第10张票 ... 窗口1正在出售第1张票 窗口2正在出售第0张票 ← Bug2:负数票 窗口3正在出售第-1张票 ``` **预期输出示例**(加 synchronized 修复后): ``` 窗口1正在出售第100张票 窗口2正在出售第99张票 窗口3正在出售第98张票 ... 窗口3正在出售第1张票 全部窗口卖票结束!共卖出100张票,无重复、无负数 ``` **要求说明**: 1. **先不加 synchronized 运行复现 Bug**(相同票多次出现、负数票),再给 `sell()` 加 `synchronized` 修复 2. 用**同步方法(锁 this)**修复后,再改用 **同步代码块锁自定义对象**(`private final Object lock = new Object(); synchronized(lock){...}`)实现第二种写法 3. 必须用**线程数组 + `join()`** 等待所有窗口线程结束,最后统计验证总票数 4. 运行后对比"无锁 vs 加锁"的输出,用注释写出你的观察结论 **💡 提示**: - **Bug 原因**:`if (tickets > 0)`(判断)与 `tickets--`(修改)之间是**读-改-写复合操作**,抢占式调度下线程随时可能被切换走,另一个线程趁机插入操作共享数据 - **synchronized 作用**:保证**同一时刻只有一个线程**执行被保护的代码(拿不到锁的线程进入 **BLOCKED** 排队等待),从而保证**原子性** - **三种写法**:①同步实例方法 `public synchronized void sell()` 锁 this;②同步静态方法 `public static synchronized` 锁 Class 对象;③同步代码块 `synchronized(lock){}` 锁指定对象(**最灵活、粒度最细**) - **锁对象必须一致**:所有窗口线程共用**同一个** SellTicket 对象(同一个锁)才有效;如果每个线程 new 一个 SellTicket,锁对象不同就失效了 - **线程安全三大特性**:原子性(防读-改-写打断→synchronized/Lock/Atomic)、可见性(防 CPU 缓存→volatile/锁)、有序性(防指令重排→volatile/synchronized) **🎯 挑战加分(选做)**: 1. 实现**同步代码块锁自定义对象**和**同步静态方法(锁 Class 对象)**两种额外写法,对比三种写法的锁对象 2. 用 `synchronized(this)` 写一个**转账方法** `transfer(int amount)`:把"检查余额 ≥ amount"和"余额 -= amount"整体锁住,模拟多个线程同时转账,验证余额不为负 3. 在 `sell()` 中加 `Thread.sleep(50)` 放大并发窗口,观察不加锁时 Bug 复现更明显;加锁后再运行验证修复效果 4. 画一张"线程安全三大特性 + 各自解决方案"表格,并把结论写在代码注释里 --- ## 四、提交要求 - **类名**:`Demo_题号_姓名`(如 `Demo_01_张三`、`Demo_02_李四`、`Demo_03_王五`) - **文件头注释**:每个 Java 文件头部必须包含 `@author`(你的姓名)、`@title`(题目名称)、`@create 2026/8/10` - **提交内容**:源码文件 + 运行结果截图 - **必做/选做**:基础题(题目 1)、进阶题(题目 2)为**必做**;挑战题(题目 3)为**选做**,鼓励学有余力的同学完成 - **运行环境**:JDK 8 及以上(挑战题使用 `synchronized` 关键字,JDK 自带)