2026年8月10日-作业.md 17 KB

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() 观察某个线程在不同阶段的状态变化,体会优先级对抢占调度的影响和生命周期状态流转。

题目要求:

/**
 * @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() 验证两个线程的守护属性。

题目要求:

/**
 * @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 / 同步代码块锁自定义对象)修复,最后验证总票数正确、无重复、无负数。

题目要求:

/**
 * @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 自带)