2026年8月8日-作业.md 13 KB

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,边加边打印线程名和当前值,最后打印各自的总和,观察并发执行效果。

题目要求:

/**
 * @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 条执行路径并发。请完整实现这三种方式并观察主线程并发。

题目要求:

/**
 * @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> 接口(泛型指定返回类型)、V call() 带返回值且声明可抛异常、FutureTask<V>(Callable<V>) 包装任务、new Thread(futureTask, name) 启动、V get() 阻塞等待获取计算结果、三种实现方式对比(run() 无返回值 vs call() 有返回值)

场景描述:前两种方式的 run() 方法没有返回值,线程计算完结果只能自己打印、无法交回主程序。方式三 Callable.call() 带返回值:请定义 SumCallable implements Callable<Integer>,让 3 个线程分别计算 1~100、1~1000、1~10000 的和,用 FutureTask.get() 把每个线程的计算结果取回,最后在 main 中汇总打印。

题目要求:

import java.util.concurrent.*;

/**
 * @author 你的姓名
 * @title 题目3:实现 Callable 带返回值求和任务
 * @create 2026/8/8
 */
public class Demo_03_姓名 {
    // ========== ① 实现 Callable 接口,泛型指定返回类型 ==========
    static class SumCallable implements Callable<Integer> {
        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<Integer> 包装每个 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>(泛型指定返回类型为 Integer),重写 call() 方法并 return 计算结果
  2. 必须用 FutureTask<Integer> 包装 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 自带)