说明:本次课程学习了 多线程 的核心内容——进程 / 线程 / 并行与并发 基础概念(
main方法即主线程)、实现多线程的三种方式:
- 方式一:继承
Thread类,重写run()(super(name)构造器传线程名、getName()、start()启动、run()与start()的区别)- 方式二:实现
Runnable接口(Thread.currentThread().getName()、Thread(Runnable, name)构造方法、实现类对象 / 匿名内部类 / Lambda 三种提供任务的方式、主线程与子线程并发)- 方式三:实现
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 文档注释 |
| 项目 | 要求 |
|---|---|
| 提交方式 | 将源码文件 + 运行结果截图提交至班级指定平台 |
| 截止时间 | 下次上课前 |
知识点:进程 / 线程概念、方式一继承 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
要求说明:
extends Thread,重写 run(),在 run() 中封装"求和 + 打印"的操作this.getName() 打印start(),不允许直接调 run()💡 提示:
super(name) 把线程名传给 Thread 父类;构造后 this.getName() 就能拿到这个名字start() 会启动两条独立执行路径,CPU 交替调度它们,所以输出交错run() 不会创建新线程——它只是普通方法调用,会先执行完线程1的 run 再执行线程2的 run(验证 run 与 start 的区别)🎯 挑战加分(选做):
new SumThread("线程1", 1000)),让两个线程求不同范围的和setName("新名字") 在启动前动态修改线程名,再打印验证Demo_01 的 .java 文件、运行中的 JVM、两个 SumThread 线程各自对应什么概念知识点:方式二实现 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
...
要求说明:
Runnable 接口(不允许继承 Thread),获取线程名用 Thread.currentThread().getName()Thread(Runnable, name) 构造方法指定不同的线程名main 主线程也要循环打印,观察主线程与子线程并发(4 种线程名交错)💡 提示:
this 不是线程对象,所以用静态方法 Thread.currentThread() 拿当前执行线程,再 .getName()new Thread(new Runnable() { @Override public void run() {...} }, "匿名线程")new Thread(() -> { ... }, "Lambda线程")——Runnable 是函数式接口(只有一个抽象方法)才能用 Lambdastart() 只是启动子线程,main 方法不等待、继续往下执行,所以主线程的循环打印会和子线程的输出混在一起🎯 挑战加分(选做):
CountTask 对象同时传给两个 Thread,观察两个线程输出内容是否完全相同(体会"共享任务对象")Thread.sleep(1)(需要 try-catch)在每次循环后休眠 1 毫秒,观察输出节奏变化知识点:方式三实现 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
要求说明:
Callable<Integer>(泛型指定返回类型为 Integer),重写 call() 方法并 return 计算结果FutureTask<Integer> 包装 Callable,再传给 Thread 构造(Thread 构造只接收 Runnable/FutureTask)ft.get() 获取每个线程的返回值并打印(含 main 汇总)get() 与 sleep() 需要处理异常(main 直接 throws Exception 即可)💡 提示:
FutureTask 是"任务容器":它包装 Callable 后交给线程执行;get() 会阻塞等待该线程计算完成再返回 call() 的结果main throws Exception 可以同时覆盖 InterruptedException(sleep)与 ExecutionException(get)run() 无返回值,结果只能在 run() 内部打印;Callable 的 call() 有返回值,结果可以交回主线程用 get() 取🎯 挑战加分(选做):
call() 中加 Thread.sleep(1000) 模拟耗时计算,观察 get() 调用前的 main 是否被"卡住"等待(体会 get 的阻塞特性)Demo_题号_姓名(如 Demo_01_张三、Demo_02_李四、Demo_03_王五)@author(你的姓名)、@title(题目名称)、@create 2026/8/8java.util.concurrent 并发包,JDK 自带)