20260810-线程操作与生命周期练习.md 23 KB

2026-08-10 线程操作与生命周期练习(课堂练习用)

说明:练习今日新学知识点——线程操作方法(Thread.sleep() 线程休眠、getPriority() / setPriority() 线程优先级、join() / join(long) 等待线程结束、interrupt() / isInterrupted() 中断标志机制、yield() 主动让出 CPU、isAlive() 判断存活)、守护线程(setDaemon() / isDaemon()、JVM 退出机制、守护线程特点)、线程生命周期(Thread.State 枚举 6 种状态:NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED、getState() 观察状态)、Object 类线程方法(wait() / notify() / notifyAll() 等待唤醒)。


练习一:线程生命周期观察 —— getState() 打印 6 种状态

难度:⭐⭐⭐ 知识点:Thread.State 枚举、getState()、线程 6 种状态(NEW / RUNNABLE / TIMED_WAITING / TERMINATED)、join()、sleep()

场景描述:课堂学了线程生命周期:Java 中线程有 6 种状态(基于 Thread.State 枚举)——新建 NEW、可运行 RUNNABLE、阻塞 BLOCKED、等待 WAITING、计时等待 TIMED_WAITING、终止 TERMINATED。本例用 getState() 方法在不同阶段打印线程状态,直观观察线程从"出生"到"死亡"的状态变化(可观察到的有 NEW / RUNNABLE / TIMED_WAITING / TERMINATED)。

案例需求

使用 getState() 方法观察一个线程在"创建→启动→休眠→结束"各阶段的状态。

实现步骤

  1. 创建一个线程对象(任务:循环打印并 sleep 500ms)
  2. start() 之前打印状态 → 预期 NEW
  3. start() 之后立即打印状态 → 预期 RUNNABLE(或运行中)
  4. 主线程 sleep 200ms 后打印子线程状态 → 预期 TIMED_WAITING(子线程正在 sleep)
  5. join() 等待子线程结束后打印状态 → 预期 TERMINATED

代码框架

public class LifecycleTest {
    public static void main(String[] args) throws InterruptedException {
        // ========== 1、创建线程对象 ==========
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(500);   // sleep 中 → TIMED_WAITING 计时等待
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("子线程工作中---->" + i);
            }
        }, "worker");

        // ========== 2、start() 之前:NEW 状态 ==========
        // TODO 1: System.out.println("start() 之前状态:" + t.getState());   // 预期 NEW

        // ========== 3、start() 之后:RUNNABLE 状态 ==========
        // TODO 2: t.start();  并打印状态   // 预期 RUNNABLE

        // ========== 4、主线程 sleep,观察子线程 sleep 期间的状态 ==========
        // TODO 3: Thread.sleep(200); 后打印 t.getState()   // 预期 TIMED_WAITING

        // ========== 5、join 等待子线程结束,打印 TERMINATED ==========
        // TODO 4: t.join(); 后打印 t.getState()   // 预期 TERMINATED
    }
}

输出示例:

start() 之前状态:NEW
start() 之后状态:RUNNABLE
子线程工作中---->0
子线程 sleep 期间状态:TIMED_WAITING
子线程工作中---->1
...
子线程结束后状态:TERMINATED

选做加分:

  • 在主线程中创建 3 个线程,分别打印它们的 getState(),对比多个线程并发时的状态
  • 在线程 run() 中调用 wait()(需 synchronized 同步块),在主线程打印它的状态观察 WAITING
  • 用两个线程竞争一个 synchronized 锁,观察未抢到锁的线程状态是否为 BLOCKED

思考题:

  1. 为什么 getState() 在 start() 之后立即调用,返回的多半是 RUNNABLE 而不是 RUNNING?(提示:Java 把"运行"和"就绪"合并成一个 RUNNABLE 状态,无法精确区分)
  2. sleep() 期间线程处于什么状态?(提示:TIMED_WAITING 计时等待,超时自动恢复)
  3. join() 让调用线程进入什么状态?(提示:WAITING 无限等待;join(timeout) 进入 TIMED_WAITING)
  4. 线程为什么 TERMINATED 之后就"回不去"了?(提示:TERMINATED 是终点站,run() 结束不可逆)

练习二:线程优先级 + sleep —— 运动员赛跑并发演示

难度:⭐⭐⭐ 知识点:getPriority() / setPriority()(默认 5、范围 1~10)、Thread.MAX_PRIORITY / NORM_PRIORITY / MIN_PRIORITY、Thread.sleep()、Lambda 创建 Runnable、抢占式调度模型、多线程并发随机性

场景描述:课堂学了线程优先级:Java 采用抢占式调度模型,优先级高的线程获得 CPU 时间片更多;线程默认优先级是 5,范围 1~10。本例用 3 个"运动员"线程赛跑,分别设置最高 / 普通 / 最低优先级,配合 sleep() 模拟跑步,观察优先级对获得 CPU 机会的影响,以及多线程执行的随机性。

案例需求

创建 3 个运动员线程(优先级分别为 10 / 5 / 1),每个跑 10 圈(每圈 sleep 100ms),观察到达终点的先后顺序。

实现步骤

  1. 用 Lambda 创建 Runnable 任务(获取当前线程名 + 优先级,循环打印圈数)
  2. 创建 3 个 Thread 对象(运动员1 / 2 / 3)
  3. 打印默认优先级(验证都是 5)
  4. 分别设置优先级:MAX_PRIORITY(10) / NORM_PRIORITY(5) / MIN_PRIORITY(1)
  5. 修改后再打印一次优先级,验证 setPriority 生效
  6. start() 启动 3 个线程,观察输出交错

代码框架

public class RaceTest {
    public static void main(String[] args) {
        // ========== 1、Lambda 创建 Runnable 任务 ==========
        Runnable runner = () -> {
            // TODO 1: 获取当前线程名和优先级
            //         用 Thread.currentThread().getName() / getPriority()
            for (int i = 1; i <= 10; i++) {
                try {
                    Thread.sleep(100);   // 模拟跑步速度
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // TODO 2: 打印 "线程名(优先级X)跑完第 i 圈"
            }
            // TODO 3: 打印 "线程名 到达终点!"
        };

        // ========== 2、创建 3 个运动员线程 ==========
        // TODO 4: new Thread(runner, "运动员1") / "运动员2" / "运动员3"

        // ========== 3、打印默认优先级(都是 5)==========
        // TODO 5: System.out.println(t1.getName() + " 默认优先级:" + t1.getPriority());

        // ========== 4、设置不同优先级 ==========
        // TODO 6: t1.setPriority(Thread.MAX_PRIORITY);    // 10
        //         t2.setPriority(Thread.NORM_PRIORITY);   // 5
        //         t3.setPriority(Thread.MIN_PRIORITY);    // 1

        // ========== 5、再次打印优先级 ==========
        // TODO 7: 打印三个线程修改后的优先级,验证 10 / 5 / 1

        // ========== 6、启动 3 个线程 ==========
        // TODO 8: t1.start(); t2.start(); t3.start();
    }
}

输出示例:

运动员1 默认优先级:5
运动员2 默认优先级:5
运动员3 默认优先级:5
运动员1 优先级改为:10
运动员2 优先级改为:5
运动员3 优先级改为:1
运动员1(优先级10)跑完第 1 圈
运动员3(优先级1)跑完第 1 圈
运动员2(优先级5)跑完第 1 圈
...

选做加分:

  • 把 setPriority(11) 传入试试,观察抛出的 IllegalArgumentException
  • 去掉 Thread.sleep(100) 再运行,观察输出速度与交错变化——sleep 放慢速度便于观察
  • 循环运行多次,统计"到达终点"的先后,观察高优先级线程(10)是否更可能先到

思考题:

  1. 线程默认优先级是多少?取值范围是多少?(提示:默认 5,范围 1~10)
  2. 高优先级线程一定先执行完吗?(提示:不一定!优先级只是提高获得 CPU 的"概率",抢占式调度 + 随机性决定实际顺序)
  3. 为什么 Java 要用抢占式调度模型?(提示:优先让优先级高的线程使用 CPU,优先级高的线程获得的时间片更多)
  4. Thread.sleep(100) 在这个例子中起什么作用?(提示:主动让出时间片、放慢速度,让并发输出交错更明显)

练习三:守护线程 —— 后台心跳监控(JVM 退出机制验证)

难度:⭐⭐⭐⭐ 知识点:setDaemon(boolean)、isDaemon()、用户线程 vs 守护线程、JVM 退出条件(所有用户线程结束才退出)、守护线程特点(start() 前设置、被强杀时 finally 不保证执行)

场景描述:课堂学了守护线程:它是服务型线程,专门为用户线程提供后台支持;只要还有一个用户线程存活,JVM 就不会退出,而只剩守护线程时 JVM 直接退出,守护线程被强制终止。本例实现一个"主任务(用户线程)+ 心跳监控(守护线程)"的场景,验证 main 结束后 JVM 是否退出、守护线程是否被强制终止。

案例需求

创建主任务线程(用户线程,执行 3 秒)和心跳监控线程(守护线程,无限循环报告心跳)。观察 main 结束后:用户线程还在跑时 JVM 是否退出?守护线程是否还在打印?

实现步骤

  1. 创建用户线程(主任务):循环 3 次,每次 sleep 1 秒打印"主任务执行中...第 N 秒"
  2. 创建守护线程(心跳监控):无限循环,每 300ms 打印一次心跳次数
  3. 在 start() 之前调用 heart.setDaemon(true)
  4. 打印两个线程的 isDaemon(),验证守护属性
  5. start() 启动两个线程,观察 main 结束后的输出

代码框架

public class DaemonTest {
    public static void main(String[] args) {
        // ========== 1、用户线程(主任务):执行 3 秒 ==========
        Thread user = new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // TODO 1: 打印 "主任务执行中...第 i 秒"
            }
            // TODO 2: 打印 "主任务执行完毕!"
        }, "主任务");

        // ========== 2、守护线程(心跳监控):无限循环 ==========
        Thread heart = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // TODO 3: 打印 "心跳检测中...第 (++count) 次"
            }
        }, "心跳监控");

        // ========== 3、设置守护线程(必须在 start 之前!)==========
        // TODO 4: heart.setDaemon(true);

        // ========== 4、验证 isDaemon ==========
        // TODO 5: 打印 heart.isDaemon()(预期 true)和 user.isDaemon()(预期 false)

        // ========== 5、启动两个线程 ==========
        // TODO 6: user.start(); heart.start();
        // main 结束后:只剩守护线程 → JVM 退出 → 心跳线程被强制终止
    }
}

输出示例:

心跳监控是否是守护线程:true
主任务是否是守护线程:false
主任务执行中...第 1 秒
心跳检测中...第 1 次
心跳检测中...第 2 次
心跳检测中...第 3 次
主任务执行中...第 2 秒
...
主任务执行完毕!
        ← main 结束,JVM 退出,心跳检测不再打印

选做加分:

  • 把 heart.setDaemon(true) 移到 heart.start() 之后,观察抛 IllegalThreadStateException
  • 把心跳线程改成用户线程(不 setDaemon),运行后观察 main 结束后 JVM 是否退出——心跳会一直打印停不下来
  • 在心跳线程 run() 的 try-finally 中打印"资源清理",观察守护线程被强杀时 finally 是否执行

思考题:

  1. JVM 什么时候退出?(提示:所有用户线程(普通线程)都结束时才退出;main 主线程也是用户线程)
  2. 为什么 main 结束了,心跳监控(守护线程)还会被强制终止?(提示:只剩守护线程时 JVM 直接结束,守护线程被强杀)
  3. setDaemon(true) 为什么必须在 start() 之前调用?(提示:线程启动后守护属性不可修改,否则抛 IllegalThreadStateException)
  4. 守护线程适合做什么任务?能不能用来做资源清理?(提示:适合心跳检测、后台日志、监控统计等;被强杀时 finally 不保证执行,不能用于资源清理)

练习四:join + interrupt + yield —— 主线程协作等待

难度:⭐⭐⭐⭐ 知识点:join() / join(long) 等待线程结束、interrupt() / isInterrupted() 中断标志机制、InterruptedException、catch 后重设中断标志、yield() 主动让出 CPU

场景描述:课堂学了线程操作方法:join() 让当前线程等待目标线程执行完毕;interrupt() 设置中断标志(不杀线程),线程在 sleep/wait/join 中被中断会抛 InterruptedException 并清除标志,catch 后通常重设标志;yield() 主动让出 CPU 时间片。本例综合运用这三种方法:① 主线程 join() 等子线程完成;② 中断一个正在 sleep 的线程;③ 子线程用 yield() 让出 CPU。

案例需求

演示 join / interrupt / yield 三种线程操作方法的协作场景。

实现步骤

  1. join 演示:创建 worker-1 线程(循环 5 次:yield 让出 CPU → 打印 → sleep 200ms),主线程 worker.join() 等它结束
  2. interrupt 演示:创建 worker-2 线程(sleep 5000ms 模拟长任务),主线程 sleep 500ms 后 interrupt() 中断它,观察 catch 中抛异常、重设标志
  3. 两个子线程结束后,主线程打印"主线程结束"

代码框架

public class CooperationTest {
    public static void main(String[] args) throws InterruptedException {
        // ========== 1、join + yield 演示 ==========
        Thread worker = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                // TODO 1: Thread.yield();   主动让出 CPU 时间片
                // TODO 2: 打印 "worker-1 工作中---> i"
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    // TODO 3: Thread.currentThread().interrupt();   重设中断标志
                    break;
                }
            }
        }, "worker-1");

        worker.start();
        // TODO 4: worker.join();   主线程等待 worker-1 结束
        // TODO 5: 打印 "worker-1 已结束,主线程继续..."

        // ========== 2、interrupt 中断演示 ==========
        Thread sleeper = new Thread(() -> {
            try {
                Thread.sleep(5000);   // 长任务:睡 5 秒
                System.out.println("任务完成");   // 被中断就不会执行到这
            } catch (InterruptedException e) {
                // TODO 6: Thread.currentThread().interrupt();   重设中断标志
                // TODO 7: 打印 "sleep 中被中断,isInterrupted()= 当前值"
            }
        }, "worker-2");

        sleeper.start();
        Thread.sleep(500);       // 让 sleeper 先进入 sleep 状态
        // TODO 8: sleeper.interrupt();   向 worker-2 发送中断请求
        sleeper.join();          // 主线程等 worker-2 处理完中断
        // TODO 9: 打印 "主线程结束"
    }
}

输出示例:

worker-1 工作中---> 0
worker-1 工作中---> 1
...
worker-1 已结束,主线程继续...
sleep 中被中断,isInterrupted()= true
主线程结束

选做加分:

  • 把 worker.join() 改成 worker.join(500),观察主线程最多等 500ms 就继续(不等 worker-1 跑完 5 轮)
  • 去掉 catch 里的 Thread.currentThread().interrupt(),观察外层 isInterrupted() 的值变化——体会"InterruptedException 抛出时标志被清除"
  • 在线程 run() 中写 while (true) + if (Thread.currentThread().isInterrupted()) break;,用 interrupt() 优雅地结束无限循环线程

思考题:

  1. join() 和 join(long) 的区别是什么?(提示:join() 无限等待目标线程结束;join(millis) 最多等待指定毫秒,超过就不等)
  2. interrupt() 会杀死线程吗?(提示:不会!只是设置中断标志,请求线程自行响应,是否退出由线程自己决定)
  3. 为什么 InterruptedException 抛出的同时会清除中断标志?(提示:阻塞方法被中断时先清标志再抛异常;catch 后重新 interrupt() 是为了让外层 isInterrupted() 能感知到中断)
  4. yield() 和 sleep(ms) 有什么区别?(提示:yield 只是让出一次 CPU 回到 RUNNABLE,可能马上又被调度;sleep 是限时休眠进入 TIMED_WAITING)

参考答案要点

练习一:LifecycleTest(getState() 观察线程状态)

// 1、start() 之前:NEW(TODO 1)
System.out.println("start() 之前状态:" + t.getState());   // NEW

// 2、start() 之后:RUNNABLE(TODO 2)
t.start();
System.out.println("start() 之后状态:" + t.getState());   // RUNNABLE

// 3、主线程 sleep 200ms 后:子线程处于 TIMED_WAITING(TODO 3)
Thread.sleep(200);
System.out.println("子线程 sleep 期间状态:" + t.getState());   // TIMED_WAITING

// 4、join 等待子线程结束:TERMINATED(TODO 4)
t.join();
System.out.println("子线程结束后状态:" + t.getState());   // TERMINATED

练习二:RaceTest(线程优先级赛跑)

// 1、Lambda 创建 Runnable 任务(TODO 1~3)
Runnable runner = () -> {
    String name = Thread.currentThread().getName();
    int priority = Thread.currentThread().getPriority();
    for (int i = 1; i <= 10; i++) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(name + "(优先级" + priority + ")跑完第 " + i + " 圈");
    }
    System.out.println(name + " 到达终点!");
};

// 2、创建 3 个运动员线程(TODO 4)
Thread t1 = new Thread(runner, "运动员1");
Thread t2 = new Thread(runner, "运动员2");
Thread t3 = new Thread(runner, "运动员3");

// 3、打印默认优先级(TODO 5)
System.out.println(t1.getName() + " 默认优先级:" + t1.getPriority());   // 5

// 4、设置不同优先级(TODO 6)
t1.setPriority(Thread.MAX_PRIORITY);    // 10
t2.setPriority(Thread.NORM_PRIORITY);   // 5
t3.setPriority(Thread.MIN_PRIORITY);    // 1

// 5、再次打印(TODO 7)
System.out.println(t1.getName() + " 优先级改为:" + t1.getPriority());   // 10
System.out.println(t2.getName() + " 优先级改为:" + t2.getPriority());   // 5
System.out.println(t3.getName() + " 优先级改为:" + t3.getPriority());   // 1

// 6、启动 3 个线程(TODO 8)
t1.start();
t2.start();
t3.start();

练习三:DaemonTest(守护线程心跳监控)

// 1、用户线程(主任务)(TODO 1~2)
Thread user = new Thread(() -> {
    for (int i = 1; i <= 3; i++) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("主任务执行中...第 " + i + " 秒");
    }
    System.out.println("主任务执行完毕!");
}, "主任务");

// 2、守护线程(心跳监控)(TODO 3)
Thread heart = new Thread(() -> {
    int count = 0;
    while (true) {
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("心跳检测中...第 " + (++count) + " 次");
    }
}, "心跳监控");

// 3、设置守护线程(start 之前)(TODO 4)
heart.setDaemon(true);

// 4、验证 isDaemon(TODO 5)
System.out.println("心跳监控是否是守护线程:" + heart.isDaemon());   // true
System.out.println("主任务是否是守护线程:" + user.isDaemon());       // false

// 5、启动(TODO 6)
user.start();
heart.start();
// main 结束 → 只剩守护线程 → JVM 退出 → 心跳被强制终止

练习四:CooperationTest(join + interrupt + yield)

// 1、join + yield 演示(TODO 1~5)
Thread worker = new Thread(() -> {
    for (int i = 0; i < 5; i++) {
        Thread.yield();   // 主动让出 CPU
        System.out.println("worker-1 工作中---> " + i);
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();   // 重设中断标志
            break;
        }
    }
}, "worker-1");

worker.start();
worker.join();   // 主线程等待 worker-1 结束
System.out.println("worker-1 已结束,主线程继续...");

// 2、interrupt 中断演示(TODO 6~9)
Thread sleeper = new Thread(() -> {
    try {
        Thread.sleep(5000);
        System.out.println("任务完成");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();   // 重设中断标志
        System.out.println("sleep 中被中断,isInterrupted()= " + Thread.currentThread().isInterrupted());
    }
}, "worker-2");

sleeper.start();
Thread.sleep(500);      // 让 sleeper 先进入 sleep
sleeper.interrupt();    // 发送中断请求
sleeper.join();         // 主线程等 worker-2 处理完中断
System.out.println("主线程结束");

涵盖知识点总览

知识点 说明
Thread.State 枚举 线程 6 种状态:NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED
getState() 返回线程当前状态的枚举值,用于观察线程生命周期
NEW → RUNNABLE new Thread() 后未 start() 是 NEW;start() 后进入 RUNNABLE(运行或等时间片)
TIMED_WAITING sleep(ms) / wait(timeout) / join(timeout) 限时等待状态
TERMINATED run() 执行完毕或抛未捕获异常结束,终点站不可逆
getPriority() 返回线程优先级(默认 5)
setPriority(int) 修改线程优先级,范围 1~10(超出抛 IllegalArgumentException)
优先级常量 MAX_PRIORITY(10) / NORM_PRIORITY(5) / MIN_PRIORITY(1)
抢占式调度模型 优先让优先级高的线程使用 CPU,同优先级随机选择;Java 默认采用
多线程随机性 谁抢到 CPU 时间片不确定,执行顺序具有随机性
setDaemon(boolean) 标记守护线程,必须在 start() 之前调用
isDaemon() 判断线程是否是守护线程
JVM 退出机制 所有用户线程结束才退出;只剩守护线程时 JVM 直接退出并强杀守护线程
守护线程特点 服务型线程、子线程默认继承守护属性、被强杀时 finally 不保证执行
join() 当前线程等待目标线程执行完毕(进入 WAITING)
join(long) 最多等待指定毫秒,超过就不等(进入 TIMED_WAITING)
interrupt() 设置中断标志不杀线程;阻塞中被中断抛 InterruptedException 并清除标志
isInterrupted() 检查中断标志(不清除);Thread.interrupted() 检查并清除
重设中断标志 catch 中 Thread.currentThread().interrupt() 恢复标志,方便外层感知
yield() 主动让出 CPU 时间片重新竞争;只是建议,调度器不一定采纳
wait() / notify() / notifyAll() Object 类线程通信方法:等待 / 唤醒(依赖对象锁,进 WAITING)