# 2026年8月8日 课后作业 — 多线程三种实现方式:继承 Thread / 实现 Runnable / 实现 Callable > **说明**:本次课程学习了 **多线程** 的核心内容——**进程 / 线程 / 并行与并发** 基础概念(`main` 方法即主线程)、**实现多线程的三种方式**: > > 1. **方式一:继承 `Thread` 类,重写 `run()`**(`super(name)` 构造器传线程名、`getName()`、`start()` 启动、`run()` 与 `start()` 的区别) > 2. **方式二:实现 `Runnable` 接口**(`Thread.currentThread().getName()`、`Thread(Runnable, name)` 构造方法、实现类对象 / 匿名内部类 / Lambda 三种提供任务的方式、主线程与子线程并发) > 3. **方式三:实现 `Callable` 接口 + `FutureTask`**(`call()` **带返回值**、泛型指定返回类型、`FutureTask` 包装、`get()` 获取计算结果) > > 以及 **三种实现方式的对比**(接口派 vs 继承派)与 **Thread 常用方法**(`setName` / `getName` / `currentThread`)。 > > 本作业共 **3 道题**:**基础题(必做)**——方式一继承 Thread 多线程求和打印器;**进阶题(必做)**——方式二实现 Runnable 三种方式提供任务;**挑战题(选做)**——方式三实现 Callable + FutureTask 带返回值求和。覆盖多线程核心知识点。 --- ## 📌 作业总体要求 ### 一、命名要求 | 项目 | 要求 | |------|------| | **类名** | `Demo_题号_你的姓名`(如 `Demo_01_张三`、`Demo_02_李四`、`Demo_03_王五`) | | **文件头注释** | 每个 Java 文件头部需包含 `@author`、`@title`、`@create` 文档注释 | ### 二、提交方式与截止时间 | 项目 | 要求 | |------|------| | **提交方式** | 将源码文件 + 运行结果截图提交至班级指定平台 | | **截止时间** | 下次上课前 | --- ## 一、基础题(必做) ### 题目 1:继承 Thread 多线程求和打印器 ThreadSumDemo(⭐⭐ 必做) **知识点**:进程 / 线程概念、方式一**继承 `Thread` 类**、重写 `run()` 封装线程操作、`super(name)` 构造器传线程名、`getName()` 获取线程名、`start()` 启动线程、`run()` vs `start()` 的区别 **场景描述**:课堂用 `MyThread` 演示了方式一(继承 Thread)——自定义线程类在 `run()` 中边求和边打印,`ThreadTest01` 创建两个线程对象用 `start()` 启动,两个线程的输出**交错出现**。请仿照实现:两个线程各自从 1 累加到 100,边加边打印线程名和当前值,最后打印各自的总和,观察并发执行效果。 **题目要求**: ```java /** * @author 你的姓名 * @title 题目1:继承 Thread 多线程求和打印器 * @create 2026/8/8 */ public class Demo_01_姓名 { // ========== ① 自定义线程类:继承 Thread ========== static class SumThread extends Thread { public SumThread(String name) { super(name); // TODO 1: 把线程名传给父类 Thread } @Override public void run() { // TODO 2: int sum = 0; 从 1 累加到 100 // TODO 3: 边加边打印 this.getName() + "--->" + i // TODO 4: 循环结束打印 this.getName() + "求和结果:" + sum } } public static void main(String[] args) { // TODO 5: 创建两个 SumThread 线程对象(如"加法线程1"、"加法线程2") // TODO 6: 调用 start() 启动两个线程,观察输出是否交错出现 // TODO 7: (思考)把 start() 改成 run() 再运行一次,对比输出顺序有什么不同 } } ``` **预期输出示例**(每次运行顺序可能不同): ``` 加法线程1--->1 加法线程2--->1 加法线程1--->2 加法线程2--->2 ... 加法线程1求和结果:5050 加法线程2求和结果:5050 ``` **要求说明**: 1. 自定义类必须 `extends Thread`,重写 `run()`,在 `run()` 中封装"求和 + 打印"的操作 2. 两个线程对象必须用构造方法传入不同线程名,用 `this.getName()` 打印 3. 启动线程必须调用 `start()`,不允许直接调 `run()` **💡 提示**: - `super(name)` 把线程名传给 Thread 父类;构造后 `this.getName()` 就能拿到这个名字 - 两个线程的 `start()` 会启动**两条独立执行路径**,CPU 交替调度它们,所以输出交错 - 直接调 `run()` 不会创建新线程——它只是普通方法调用,会先执行完线程1的 run 再执行线程2的 run(验证 run 与 start 的区别) **🎯 挑战加分(选做)**: 1. 把求和上限改成构造参数传入(如 `new SumThread("线程1", 1000)`),让两个线程求不同范围的和 2. 用 `setName("新名字")` 在启动前动态修改线程名,再打印验证 3. 用"程序 / 进程 / 线程"解释运行后:`Demo_01` 的 `.java` 文件、运行中的 JVM、两个 SumThread 线程各自对应什么概念 --- ## 二、进阶题(必做) ### 题目 2:实现 Runnable 三种方式提供任务 RunnableCountDemo(⭐⭐⭐ 必做) **知识点**:方式二**实现 `Runnable` 接口**、重写 `run()`、`Thread.currentThread().getName()`(不继承 Thread 时获取线程名)、`Thread(Runnable task, String name)` 构造方法指定线程名、**实现类对象 / 匿名内部类 / Lambda 表达式**三种提供任务的方式、`start()` 后**主线程不阻塞继续执行**(主线程与子线程并发) **场景描述**:课堂用 `RunnableImpl` + `ThreadTest02` 演示了方式二——`Runnable` 接口只有一个抽象方法 `run()`,是**函数式接口**,所以能用实现类对象、**匿名内部类**、**Lambda 表达式**三种方式提供任务;三个子线程 `start()` 后 **main 主线程也继续循环打印**,共 4 条执行路径并发。请完整实现这三种方式并观察主线程并发。 **题目要求**: ```java /** * @author 你的姓名 * @title 题目2:实现 Runnable 三种方式提供任务 * @create 2026/8/8 */ public class Demo_02_姓名 { // ========== ① 实现 Runnable 接口的类 ========== static class CountTask implements Runnable { @Override public void run() { // TODO 1: String name = Thread.currentThread().getName(); 获取当前线程名 // TODO 2: 循环 100 次打印 "我是" + name + "--->" + i } } public static void main(String[] args) { // ========== ② 三种方式提供 Runnable ========== // TODO 3: 方式一(实现类对象):new CountTask(),再 new Thread(countTask, "任务线程") // TODO 4: 方式二(匿名内部类):new Thread(new Runnable(){ run(){...} }, "匿名线程") // TODO 5: 方式三(Lambda):new Thread(() -> { ... }, "Lambda线程") // TODO 6: 三个线程 start() 启动 // ========== ③ 主线程也参与打印(观察并发) ========== // TODO 7: main 中用 Thread.currentThread().getName() 拿到主线程名并循环打印 // TODO 8: 观察控制台是否出现 4 种线程名(任务线程/匿名线程/Lambda线程/main)输出交错混排 } } ``` **预期输出示例**(每次运行顺序可能不同): ``` 我是main--->0 我是任务线程--->0 我是匿名线程--->0 我是Lambda线程--->0 我是任务线程--->1 我是main--->1 ... ``` **要求说明**: 1. 必须实现 `Runnable` 接口(不允许继承 Thread),获取线程名用 `Thread.currentThread().getName()` 2. 三种提供任务的方式都必须写出来:实现类对象、匿名内部类、Lambda 表达式 3. 每个线程都要用 `Thread(Runnable, name)` 构造方法指定不同的线程名 4. `main` 主线程也要循环打印,观察主线程与子线程并发(4 种线程名交错) **💡 提示**: - 实现 Runnable 的类 `this` 不是线程对象,所以用**静态方法** `Thread.currentThread()` 拿当前执行线程,再 `.getName()` - 匿名内部类写法:`new Thread(new Runnable() { @Override public void run() {...} }, "匿名线程")` - Lambda 写法:`new Thread(() -> { ... }, "Lambda线程")`——Runnable 是**函数式接口**(只有一个抽象方法)才能用 Lambda - `start()` 只是启动子线程,main 方法**不等待**、继续往下执行,所以主线程的循环打印会和子线程的输出混在一起 **🎯 挑战加分(选做)**: 1. 同一个 `CountTask` 对象同时传给两个 `Thread`,观察两个线程输出内容是否完全相同(体会"共享任务对象") 2. 把循环次数改为 10000,观察四个线程的输出**交错密度**,理解并发切换 3. 用 `Thread.sleep(1)`(需要 try-catch)在每次循环后休眠 1 毫秒,观察输出节奏变化 --- ## 三、挑战题(选做) ### 题目 3:实现 Callable + FutureTask 带返回值求和 SumCallableDemo(⭐⭐⭐⭐ 选做) **知识点**:方式三**实现 `Callable` 接口**(泛型指定返回类型)、`V call()` **带返回值**且声明可抛异常、`FutureTask(Callable)` 包装任务、`new Thread(futureTask, name)` 启动、`V get()` 阻塞等待获取计算结果、**三种实现方式对比**(`run()` 无返回值 vs `call()` 有返回值) **场景描述**:前两种方式的 `run()` 方法**没有返回值**,线程计算完结果只能自己打印、无法交回主程序。方式三 `Callable.call()` **带返回值**:请定义 `SumCallable implements Callable`,让 3 个线程分别计算 `1~100`、`1~1000`、`1~10000` 的和,用 `FutureTask.get()` 把每个线程的**计算结果取回**,最后在 main 中**汇总打印**。 **题目要求**: ```java import java.util.concurrent.*; /** * @author 你的姓名 * @title 题目3:实现 Callable 带返回值求和任务 * @create 2026/8/8 */ public class Demo_03_姓名 { // ========== ① 实现 Callable 接口,泛型指定返回类型 ========== static class SumCallable implements Callable { private int n; // 求 1~n 的和 public SumCallable(int n) { this.n = n; } @Override public Integer call() throws Exception { // TODO 1: int sum = 0; 计算 1~n 的累加和 // TODO 2: 打印 Thread.currentThread().getName() + "求和结果:" + sum // TODO 3: return sum; // 返回计算结果(get() 会拿到这个值) } } public static void main(String[] args) throws Exception { // TODO 4: 创建 3 个 SumCallable 对象(求 1~100 / 1~1000 / 1~10000 的和) // TODO 5: 用 FutureTask 包装每个 Callable 对象 // TODO 6: new Thread(futureTask, "线程名") 创建 3 个线程并 start() // TODO 7: 调用 ft.get() 获取每个线程的返回值,打印并在 main 中汇总总结果 // TODO 8: (思考)对比三种实现方式:run() 无返回值 vs call() 有返回值,各自适合什么场景 } } ``` **预期输出示例**(每次运行顺序可能不同): ``` 线程1求和结果:5050 线程2求和结果:500500 线程3求和结果:50005000 线程1---->5050 线程2---->500500 线程3---->50005000 三个线程求和汇总:505055050 ``` **要求说明**: 1. 必须实现 `Callable`(泛型指定返回类型为 Integer),重写 `call()` 方法并 `return` 计算结果 2. 必须用 `FutureTask` 包装 Callable,再传给 `Thread` 构造(Thread 构造只接收 Runnable/FutureTask) 3. 必须用 `ft.get()` 获取每个线程的返回值并打印(含 main 汇总) 4. `get()` 与 `sleep()` 需要处理异常(main 直接 `throws Exception` 即可) **💡 提示**: - `FutureTask` 是"任务容器":它包装 Callable 后交给线程执行;`get()` 会**阻塞等待**该线程计算完成再返回 `call()` 的结果 - 三个线程可以用**同一个** SumCallable(结果相同),也可以用**三个不同范围**的(更直观地看到不同返回值) - `main throws Exception` 可以同时覆盖 `InterruptedException`(sleep)与 `ExecutionException`(get) - 对比:继承 Thread / 实现 Runnable 的 `run()` **无返回值**,结果只能在 run() 内部打印;Callable 的 `call()` **有返回值**,结果可以交回主线程用 `get()` 取 **🎯 挑战加分(选做)**: 1. 在 `call()` 中加 `Thread.sleep(1000)` 模拟耗时计算,观察 `get()` 调用前的 main 是否被"卡住"等待(体会 get 的阻塞特性) 2. 用 **匿名内部类** / **Lambda** 实现 Callable(不建 SumCallable 类),对比三种书写方式 3. 画一张"三种实现方式对比"表格(有无返回值 / 能否继承其他类 / 能否直接用 getName / 编程难度),并把结论写在代码注释里 --- ## 四、提交要求 - **类名**:`Demo_题号_姓名`(如 `Demo_01_张三`、`Demo_02_李四`、`Demo_03_王五`) - **文件头注释**:每个 Java 文件头部必须包含 `@author`(你的姓名)、`@title`(题目名称)、`@create 2026/8/8` - **提交内容**:源码文件 + 运行结果截图 - **必做/选做**:基础题(题目 1)、进阶题(题目 2)为**必做**;挑战题(题目 3)为**选做**,鼓励学有余力的同学完成 - **运行环境**:JDK 8 及以上(方式三使用 `java.util.concurrent` 并发包,JDK 自带)