# 20260808 课堂笔记 — 转换流、序列化与多线程入门 - **日期**:2026-08-08 - **项目**:`c260808` - **包路径**:`course` / `homework0807` - **作者**:WanJL --- ## 目录 1. [转换流概念:字符集编码 / 解码与乱码成因(Demo01)](#1-转换流概念字符集编码--解码与乱码成demo01) 2. [InputStreamReader —— 字节流到字符流的桥梁](#2-inputstreamreader--字节流到字符流的桥梁) 3. [OutputStreamWriter —— 字符流到字节流的桥梁](#3-outputstreamwriter--字符流到字节流的桥梁) 4. [转换流综合案例:UTF-8 文件转 GBK 文件(Demo01 main)](#4-转换流综合案例utf-8-文件转-gbk-文件demo01-main) 5. [对象持久化概念:把内存中的对象保存到磁盘(Demo02)](#5-对象持久化概念把内存中的对象保存到磁盘demo02) 6. [对象序列化:ObjectOutputStream + writeObject()](#6-对象序列化objectoutputstream--writeobject) 7. [对象反序列化:ObjectInputStream + readObject()](#7-对象反序列化objectinputstream--readobject) 8. [Serializable 标识接口与 serialVersionUID 版本兼容(InvalidClassException)](#8-serializable-标识接口与-serialversionuid-版本兼容invalidclassexception) 9. [transient 关键字:跳过序列化的属性](#9-transient-关键字跳过序列化的属性) 10. [序列化注意事项:静态属性、引用链、构造方法、单例(Demo02 更新)](#10-序列化注意事项静态属性引用链构造方法单例demo02-更新) 11. [IO 流体系总结:基础流 vs 处理流全景(Demo03)](#11-io-流体系总结基础流-vs-处理流全景demo03) 12. [Student 实体类 —— 可序列化对象综合演示](#12-student-实体类--可序列化对象综合演示) 13. [序列化 / 反序列化运行演示:ObjectOutputStreamTest / ObjectInputStreamTest](#13-序列化--反序列化运行演示objectoutputstreamtest--objectinputstreamtest) 14. [课后作业回顾:接口化文件复制效率对比器(homework0807/Demo_03)](#14-课后作业回顾接口化文件复制效率对比器homework0807demo_03) 15. [多线程入门:进程、线程、并行与并发、main 主线程(Demo04 与 Hello)](#15-多线程入门进程线程并行与并发main-主线程demo04-与-hello) 16. [实现多线程方式一:继承 Thread 类,重写 run()(MyThread 与 ThreadTest01)](#16-实现多线程方式一继承-thread-类重写-runmythread-与-threadtest01) 17. [实现多线程方式二:实现 Runnable 接口(RunnableImpl 与 ThreadTest02)](#17-实现多线程方式二实现-runnable-接口runnableimpl-与-threadtest02) 18. [实现多线程方式三:实现 Callable 接口 + FutureTask(CallableImpl 与 ThreadTest03)](#18-实现多线程方式三实现-callable-接口--futuretaskcallableimpl-与-threadtest03) 19. [知识点全景总结](#19-知识点全景总结) 20. [随堂练习要点](#20-随堂练习要点) 21. [拓展阅读](#21-拓展阅读) --- ## 1. 转换流概念:字符集编码 / 解码与乱码成因(Demo01) ### 概念 —— 为什么要转换流 上一日学习的字符流使用**平台默认字符集**读写文件。但现实中会遇到这种情况:一个文件本身是 **GBK 编码**的,而我们手头只能使用 **UTF-8** 字符集,想要给这个文件追加写入内容——直接写必然乱码。这时就需要**转换流**:把 A 字符集的文件,转换为 B 字符集的文件。 ```java // 来源:course/Demo01.java(注释部分) /* 我们讲字符流的时候,说过字符集的概念, 不同的字符集虽然都能转换成字节(二进制数)但是,同一个字节,在不同的字符集中表示的含义是不一样的。 字符--编码-->字节--解码-->字符 如果使用字符集A进行编码,使用字符集B进行解码,就会出现字符集乱码。 */ ``` ### 编码 / 解码模型 ``` 字符 --编码--> 字节 --解码--> 字符 ``` > **要点**: > - **编码(Encoding)**:字符 → 字节(字符集编码表负责把文字符号转成二进制数) > - **解码(Decoding)**:字节 → 字符(把二进制数按编码表还原成文字符号) > - **乱码根源**:不同字符集虽然都能把字符转成字节,但**同一个字节在不同字符集中表示的含义不一样**——用字符集 A 编码、用字符集 B 解码,就会得到错误字符(乱码) > - **转换流的应用场景**:把 GBK 编码的文件内容转成 UTF-8(或反之),实现不同字符集文件之间的转换 --- ## 2. InputStreamReader —— 字节流到字符流的桥梁 ### 概念 —— 读取字节并用指定字符集解码为字符 `InputStreamReader` 是**字节流到字符流的桥梁**,父类是 `Reader`。它**读取字节**,并使用**指定的字符集**把它**解码为字符**。使用的字符集可以**按名称指定**,也可以直接使用**平台默认字符集**。 ```java // 来源:course/Demo01.java(注释部分) /* InputStreamReader:是字节流到字符流的桥梁,父类是Reader 他是读取字节,并且使用指定的字符集把它解码为字符。 它使用的字符集可以有名称指定,也可以直接使用平台默认字符集。 构造方法: public InputStreamReader(InputStream in) 使用默认字符集创建InputStreamReader对象 public InputStreamReader(InputStream in, String charsetName) 使用charsetName指定的字符集创建InputStreamReader对象 */ ``` > **要点**: > - **角色定位**:字节流 → 字符流的"桥",本质上是一个 Reader,但它底层吃的是字节 > - **核心能力**:读取底层字节流时按指定字符集**解码**成字符,解决"用 A 字符集编码的字节、用 A 字符集读取"的配对问题 > - **构造方法两种**:不传字符集用平台默认;传字符集名称(如 `"UTF-8"`、`"GBK"`)则按指定字符集解码 --- ## 3. OutputStreamWriter —— 字符流到字节流的桥梁 ### 概念 —— 读取字符并用指定字符集编码为字节 `OutputStreamWriter` 是**字符流到字节流的桥梁**,父类是 `Writer`。它**读取字符**,使用**指定的字符集**把它**编码为字节**,再交给底层字节流写入。 ```java // 来源:course/Demo01.java(注释部分) /* OutputStreamWriter:是字符流到字节流的桥梁,父类是Writer 它是读取字符,使用指定的字符集把它编码为字节。 构造方法: public OutputStreamWriter(OutputStream out) 使用默认字符集创建OutputStreamWriter对象 public OutputStreamWriter(OutputStream out, String charsetName) 使用charsetName指定的字符集创建OutputStreamWriter对象 */ ``` ### 两张"桥梁"对照表 | 桥梁 | 父类 | 方向 | 动作 | 底层参数 | 典型使用 | |------|------|------|------|----------|----------| | `InputStreamReader` | `Reader` | 字节 → 字符 | **解码** | `InputStream` | 读取文件字节按指定字符集解码为字符 | | `OutputStreamWriter` | `Writer` | 字符 → 字节 | **编码** | `OutputStream` | 字符内容按指定字符集编码为字节写入文件 | > **要点**: > - **命名规律**:`XxxReader` 是输入(读)、`XxxWriter` 是输出(写),与 FileReader / FileWriter 命名一致 > - **构造参数是字节流**:`new InputStreamReader(new FileInputStream(...), "UTF-8")` —— 先用文件字节流打开文件,再包上转换流,并指定字符集 > - **与字符流的关系**:字符流(FileReader/FileWriter)本质上就是"字节流 + 默认字符集",而转换流允许**手动指定字符集**,更加灵活 --- ## 4. 转换流综合案例:UTF-8 文件转 GBK 文件(Demo01 main) ### 案例目标 使用 UTF-8 字符集打开 `abc.txt` 文件,并把内容转换成 GBK 字符集写入到 `hello.txt` 文件。 ```java // 来源:course/Demo01.java public static void main(String[] args) throws IOException { // 使用UTF-8字符集打开abc.txt文件(读端:字节→字符,按UTF-8解码) InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\abc.txt"), "UTF-8"); // 把内容使用GBK字符集写入到hello.txt文件(写端:字符→字节,按GBK编码) OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\hello.txt"), "GBK"); char[] chars = new char[1024]; int count; while ((count = isr.read(chars)) != -1) { // 按 UTF-8 解码读出字符 osw.write(chars, 0, count); // 按 GBK 编码写出字符 } osw.close(); isr.close(); } ``` ### 转码流水线 ``` D:\abc.txt(UTF-8字节) → FileInputStream → InputStreamReader(UTF-8解码) → char[] ↓ char[] → OutputStreamWriter(GBK编码) → FileOutputStream → D:\hello.txt(GBK字节) ``` > **要点**: > - **读写两端各自指定字符集**:读端 `InputStreamReader` 指定 UTF-8(源文件编码),写端 `OutputStreamWriter` 指定 GBK(目标文件编码),完成"转码" > - **与字符缓冲流相同的循环读法**:`char[1024]` 数组 + `while ((count = isr.read(chars)) != -1)` 批量读写(对应字符流的 read(char[]) 批量读) > - **转换流属于扩展流(处理流)**:它"处理"的是底层字节流(FileInputStream / FileOutputStream),符合上一日总结的"基础流 + 扩展流"组织方式 > - **方法抛 IOException**:main 直接 `throws IOException`,由 JVM 处理(教学简化写法) --- ## 5. 对象持久化概念:把内存中的对象保存到磁盘(Demo02) ### 概念 —— 为什么要对象持久化 类文件(.java / .class)可以分享给其他人,但**根据类创建的对象**是在程序运行时于**内存中开辟空间**创建的,无法直接分享。通过**对象持久化**,可以把内存中的对象**保存到磁盘**,或者**在网络中传输对象**,从而分享给他人。 ```java // 来源:course/Demo02.java(注释部分) /* 我们可以把一个类文件分享给其他人,但是如果是根据某个类创建的对象,想要分享给其他人,就不容易了 因为它是程序运行时,在内存中开辟空间创建的对象。 但是我们可以通过【对象持久化】的方式,把内存中的对象,保存到磁盘中,分享其他人。 对象持久化:就是把对象保存到磁盘、或者在网络中传输对象。 这种机制就是使用一个字节序列表示一个对象,这个字节序列包含: 对象的类型 对象的数据,和对象中存储的属性等等信息。 把字节序列写入到文件中后,就相当于永久的保存了一个对象。 反过来,可以使用【对象反序列化】的方式再从文件中读取出对象,进行对象重构。 */ ``` ### 序列化 / 反序列化模型 ``` 序列化(保存):对象(内存) --对象序列化流 ObjectOutputStream--> 字节序列 --> 磁盘文件(永久保存) 反序列化(读取):磁盘文件 --对象反序列化流 ObjectInputStream--> 字节序列 --> 内存中重新构造对象 ``` > **要点**: > - **对象持久化**:把内存中的对象保存到磁盘、或在网络中传输对象——本质是把"对象"变成**字节序列** > - **字节序列的内容**:包含**对象的类型**、**对象的数据**、**对象中存储的属性**等信息,所以反序列化时能完整还原对象 > - **序列化**:对象 → 字节序列 → 文件,**永久保存**对象 > - **反序列化**:从文件读取字节序列 → 在内存中**重构对象** > - 类比:序列化像"拍快照存档",反序列化像"读档恢复" --- ## 6. 对象序列化:ObjectOutputStream + writeObject() ### 概念 —— 对象序列化流 进行对象序列化的方式是使用**对象序列化流** `ObjectOutputStream`,它把对象写入到字节输出流中: ```java // 来源:course/Demo02.java(注释部分) /* 进行对象序列化的方式就是使用【对象序列化流】 ObjectOutputStream 构造方法: public ObjectOutputStream(OutputStream out) 创建一个写入指定OutputStream的对象序列化流对象 对象序列化的方法: public final void writeObject(Object obj) 将指定对象obj写入到ObjectOutputStream */ ``` ### 前提 —— 必须实现 Serializable 接口 ```java // 来源:course/Demo02.java(注释部分) /* 一个对象想要被序列化,必须要实现一个接口 java.io.Serializable Serializable接口没有任何需要重写的方法,也没有任何属性,它就像一个标识,标志着这个类的对象可以被序列化。 */ ``` > **要点**: > - **构造方法**:`new ObjectOutputStream(new FileOutputStream("D:/studentObject.txt"))` —— 同样先用文件字节输出流,再包上对象流 > - **核心方法**:`writeObject(Object obj)` 把任意对象写入流 > - **Serializable 是标记接口(标识接口)**:没有任何抽象方法、没有任何属性,它像一个"通行证标识",标志该类的对象**可以被序列化**——凡是支持序列化的类都必须实现它 --- ## 7. 对象反序列化:ObjectInputStream + readObject() ### 概念 —— 对象反序列化流 `ObjectInputStream` 把对象序列化后的文件进行**反序列化**,返回对象: ```java // 来源:course/Demo02.java(注释部分) /* 进行对象反序列化的方式就是使用【对象反序列化流】 ObjectInputStream ObjectInputStream是把对象序列化后的文件进行反序列化,返回对象。 public ObjectInputStream(InputStream in) 创建从InputStream中读取的反序列化对象 public final Object readObject() 从ObjectInputStream读取一个对象 */ ``` > **要点**: > - **构造方法**:`new ObjectInputStream(new FileInputStream("D:/studentObject.txt"))` 读取序列化文件 > - **核心方法**:`readObject()` 返回 `Object` 类型——需要**强转**为实际类型才能使用 > - **编译期异常**:`readObject()` 会抛 `ClassNotFoundException`(找不到对应类时),所以 main 要 `throws IOException, ClassNotFoundException` --- ## 8. Serializable 标识接口与 serialVersionUID 版本兼容(InvalidClassException) ### 经典场景 —— 类版本变化后反序列化报错 ```java // 来源:course/Demo02.java(注释部分) /* 如果 第1天 张三,通过Student类,创建s对象,使用对象序列化把对象持久化保存,并且发送给李四。 李四收到序列化的文件。 张三 修改了Student类,修改了类的属性,比如增加了一个sex性别,移除了age年龄。 李四再进行反序列化,会得到什么? 答:会报错,抛异常:java.io.InvalidClassException 怎么解决? 方案一:重新序列化 方案二:为对象所属的类增加一个属性: private static final long serialVersionUID =42L; 完成,再进行序列化后,修改属性,再反序列化的时候,就不会报错抛异常,而是正常显示, 有这个属性,就正常赋值,没有就赋值默认值,比如: Student{id=1001, name='张三', age=null} */ ``` ### 解决思路 | 方案 | 说明 | |------|------| | **重新序列化** | 类变了就用新类重新创建对象、重新保存,但丢失了旧数据 | | **固定 `serialVersionUID`** | 在类中显式声明 `private static final long serialVersionUID = 42L;`,类结构变化时也能正常反序列化 | > **要点**: > - **InvalidClassException**:序列化时类含隐式生成的版本号,反序列化时如果类的结构(属性)变了,JVM 认为"版本不一致"直接抛异常 > - **serialVersionUID 的作用**:显式固定类的"版本号",序列化 / 反序列化两端版本号一致即可兼容;类属性变了,**有这个属性就正常赋值,没有就赋默认值**(如 `age=null`) > - **推荐习惯**:凡是实现 Serializable 的类,都**显式声明 serialVersionUID**,避免类升级导致旧数据无法读取 ### serialVersionUID 的三大作用(Demo02 更新) 代码更新后,`Demo02` 补充了 `serialVersionUID` 的**底层机制**说明——为什么类结构变化会导致反序列化失败: ```java // 来源:course/Demo02.java(注释部分,本次更新新增) /* serialVersionUID的作用: 1、用来验证反序列化的时候类的版本一致性 2、如果我们没有自己定义这个属性,那么JVM会根据类型的结构自己定义serialVersionUID。 3、如果JVM定义serialVersionUID,然后类的结构发生了改变,就会导致serialVersionUID值也发生变化。 Student类--JVM假设 定义的serialVersionUID是123456L,序列化对象,对象的serialVersionUID也是123456L Student类的结构发生了改变,serialVersionUID值也会发生改变,比如变为 456789L。 再进行反序列化的时候,对比两个serialVersionUID值发现不一致,就会出现异常java.io.InvalidClassException 最好的方式就是我们自己显式声明serialVersionUID属性,至于是什么值,无所谓。 private static final long serialVersionUID =42L; */ ``` > **要点**: > - **作用 1——版本一致性校验**:`serialVersionUID` 用来验证**反序列化时类的版本是否一致**(序列化端与反序列化端是否为同一个版本) > - **作用 2——JVM 自动生成**:如果我们**没有自己定义**这个属性,JVM 会根据**类的结构**自动生成一个 `serialVersionUID` > - **作用 3——类结构变化导致版本号漂移**:由 JVM 生成的版本号与类结构强相关——类结构一变,自动版本号就变(如 `123456L → 456789L`);反序列化时对比两端版本号**不一致**,就抛 `InvalidClassException` > - **最佳实践**:**自己显式声明** `serialVersionUID`,至于是什么值**无所谓**(任意 long),只要序列化 / 反序列化两端一致即可——这就是"显式声明版本号能免疫类结构变化"的根本原因 --- ## 9. transient 关键字:跳过序列化的属性 ### 概念 —— 不希望参与序列化的成员 如果类中**某个属性不想要被序列化**(例如敏感信息、临时数据),可以给该成员加 `transient` 关键字修饰,序列化时这个属性**不会参与序列化**: ```java // 来源:course/Demo02.java(注释部分) /* 如果类中有某个属性不行被序列化,可以给这个成员添加transient关键字修饰: private transient Integer id; 那么在序列化的时候,这个属性。就不会参与序列化。 */ ``` > **要点**: > - **transient 修饰成员变量**:`private transient Integer id;` —— 序列化时跳过该属性 > - **反序列化结果**:被 transient 修饰的属性,反序列化后为**默认值**(引用类型为 null、基本类型为 0/false) > - **典型用途**:密码、缓存、与对象状态无关的临时数据等不宜持久化的字段 --- ## 10. 序列化注意事项:静态属性、引用链、构造方法、单例(Demo02 更新) ### 概念 —— 序列化的 6 条使用须知 代码更新后,`Demo02` 补充了**序列化的注意事项**,总结了使用对象序列化时必须注意的 6 条规则: ```java // 来源:course/Demo02.java(注释部分,本次更新新增) /* 序列化的注意事项: 1、静态属性不能序列化,因为静态属性是属于类的,不是属于某个对象的。 2、transient修饰的属性不能序列化 3、父类实现了java.io.Serializable接口,那么子类可以自动序列化。 4、反序列化不会调用构造方法 5、如果对象引用了其他的对象,那么引用链上的所有的对象必须是可以序列化的。 6、单例对象需要重写一个方法readResolve()方法 */ ``` ### 6 条注意事项逐条解读 | # | 注意事项 | 解读 | |---|----------|------| | 1 | **静态属性不能序列化** | `static` 成员**属于类、不属于某个对象**,序列化的是"对象"的字段,所以静态属性天然被排除在外 | | 2 | **transient 属性不能序列化** | 与第 9 节呼应:`transient` 修饰的成员变量序列化时被跳过,反序列化为默认值 | | 3 | **父类实现 Serializable → 子类自动可序列化** | 继承关系中,父类实现了 `Serializable`,子类**无需重复实现**即可被序列化(子类自动继承了"可序列化"能力) | | 4 | **反序列化不会调用构造方法** | `readObject()` 是按字节序列**在内存中直接重构对象**,**不走构造方法**(也不走实例初始化代码块)——这也是第 6 条单例问题的根源 | | 5 | **引用链上所有对象必须可序列化** | 对象引用其他对象时,序列化会把**引用链上的对象一并序列化**;链上任一对象未实现 `Serializable`,就抛 `NotSerializableException` | | 6 | **单例对象需重写 `readResolve()`** | 单例类被反序列化时,会绕过 `private` 构造器**直接重构出另一个实例**,破坏单例;重写 `readResolve()` 返回既有单例,可保持全局唯一 | > **要点**: > - 第 1、2 条是"**哪些成员不参与序列化**":`static`(属于类)+ `transient`(显式排除)——这也是 08-07 拓展阅读中"static 本身就不参与序列化"的正式总结 > - 第 4 条是理解"**反序列化为什么能绕过构造器**"的关键:对象重构完全依赖字节序列,不经过任何构造逻辑 > - 第 5 条常见报错:`NotSerializableException` 会点名是**引用链上哪个类**没实现 Serializable,排查时顺着引用链逐级检查即可 > - 第 6 条的 `readResolve()` 是单例 + 序列化的经典组合:重写后反序列化返回的仍是内存中唯一的那个实例 --- ## 11. IO 流体系总结:基础流 vs 处理流全景(Demo03) ### 概念 —— 新增 Demo03 对 IO 流体系的完整总结 `Demo03` 是本日(乃至整个 IO 流阶段)知识的**总收口**:把之前学过的所有 IO 流按"基础流 / 处理流"两大维度完整归类,并汇总了**常用方法、特殊方法、特殊属性、特殊关键字**: ```java // 来源:course/Demo03.java(本次更新新增文件) /* 基础流: 字节流: 字节输入流:InputStream |-文件字节输入流:FileInputStream 字节输出流:OutputStream |-文件字节输出流:FileOutputStream 字符流: 字符输入流:Reader |-文件字符输入流:FileReader 字符输出流:Writer |-文件字符输出流:FileWriter 处理流: 缓冲流: 缓冲字节流:BufferedInputStream / BufferedOutputStream 缓冲字符流:BufferedReader / BufferedWriter 转换流: 输入:InputStreamReader---把字节流根据指定的字符集转换为字符 输出:OutputStreamWriter--把字符根据指定的字符集转化为字节流 对象流: 序列化:ObjectOutputStream 反序列化:ObjectInputStream */ ``` ### IO 流体系全景图(Demo03 汇总版) ``` ┌─ 字节流(处理任意类型文件) │ ├─ 输入:InputStream → FileInputStream │ └─ 输出:OutputStream → FileOutputStream 基础流 ───────────────┤ │ ├─ 输入:Reader → FileReader └─ 字符流(处理纯文本文件) └─ 输出:Writer → FileWriter ├─ 缓冲流(给字节/字符流加缓冲) │ ├─ BufferedInputStream / BufferedOutputStream │ └─ BufferedReader / BufferedWriter IO 流 ─── 处理流 ──────────────────────┤ ├─ 转换流(字节 ↔ 字符 + 指定字符集) │ ├─ 输入:InputStreamReader(字节→字符,解码) │ └─ 输出:OutputStreamWriter(字符→字节,编码) │ └─ 对象流(对象持久化 / 序列化) ├─ 序列化:ObjectOutputStream └─ 反序列化:ObjectInputStream ``` ### 常用方法 / 特殊方法 / 特殊属性 / 特殊关键字汇总 ```java // 来源:course/Demo03.java(注释部分) /* 常用的方法: write() 把内存中的数据写入到文件 read() 从文件中读取数据到内存 flush() 强制刷新缓冲流缓冲区的数据到文件 close() 关闭流,释放资源 特殊方法: readLine() 读取一行数据,缓冲输入字符流特有的 newLine() 根据不同的系统进行一次换行。 writeObject() 对象序列化--把内存中的对象保存到文件中 readObject() 反序列化--从文件中读取并在内存中重构对象,不走构造方法。 特殊属性: private static final long serialVersionUID 序列化的版本UID,用来验证反序列化和序列化的类版本一致性 特殊的关键字: transient 修饰属性,让属性不参与对象序列化 */ ``` > **要点**: > - **四大流家族齐了**:缓冲流(08-07)、字符流 / 字符缓冲流(08-07)、转换流(本日)、对象流(本日)全部归入"处理流",共同特点是**包装底层基础流**、增加增强能力 > - **命名规律总结**:文件流子类以父类名作后缀(FileInputStream / FileReader...);缓冲流以 `Buffered` 前缀命名;转换流是 `InputStreamReader / OutputStreamWriter`;对象流是 `ObjectInputStream / ObjectOutputStream` > - **四个通用方法**:`write()` / `read()` / `flush()` / `close()` 是所有流共用的基本功 > - **四组特殊方法**:`readLine()` / `newLine()`(缓冲字符流特有)、`writeObject()` / `readObject()`(对象流特有)——`readObject()` 重提"不走构造方法"(对应第 10 节第 4 条) > - **一个特殊属性 + 一个特殊关键字**:`serialVersionUID`(版本一致性)+ `transient`(跳过序列化)——对象流专属的"配置项" --- ## 12. Student 实体类 —— 可序列化对象综合演示 ### 实现 —— 把今天所有知识点落到实体类上 `Student` 类是演示对象序列化的实体类,集中体现了 serialVersionUID、transient、Serializable、equals/hashCode/toString 等知识点: ```java // 来源:course/Student.java public class Student implements Serializable { // 增加一个属性:固定序列化版本号(类结构变化也能兼容反序列化) private static final long serialVersionUID = 42L; // transient:id 不参与序列化(反序列化后为 null) private transient Integer id; private String name; private Integer age; // private String sex; // 注释掉的属性:演示"类结构变化"场景 public Student() { } public Student(Integer id, String name) { this.id = id; this.name = name; this.age = age; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public boolean equals(Object o) { if (!(o instanceof Student student)) return false; return Objects.equals(id, student.id) && Objects.equals(name, student.name) && Objects.equals(age, student.age); } @Override public int hashCode() { return Objects.hash(id, name, age); } @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } } ``` > **要点**: > - **`implements Serializable`**:类实现标记接口,允许对象被序列化(本类头注释明确说明) > - **`serialVersionUID = 42L`**:显式固定版本号,类属性增删后旧数据仍可反序列化(对应 Demo02 的经典场景) > - **`private transient Integer id`**:id 不参与序列化,反序列化后为 null(可在控制台打印 `Student{id=null, ...}` 验证) > - **`sex` 属性被注释**:与 Demo02 注释中"增加 sex、移除 age"的场景呼应,演示类结构变化对反序列化的影响 > - **equals / hashCode / toString 重写**:用 `Objects.equals` / `Objects.hash` 工具方法实现(前面课程学过的 Object 类知识点复用) --- ## 13. 序列化 / 反序列化运行演示:ObjectOutputStreamTest / ObjectInputStreamTest ### 写端 —— ObjectOutputStreamTest 序列化(注释保留) ```java // 来源:course/ObjectOutputStreamTest.java(活动代码已注释保留) public static void main(String[] args) throws IOException { // 创建对象 Student s = new Student(1001, "张三", "男"); // 序列化对象 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:/studentObject.txt")); // 调用方法,进行序列化---把对象写入到文件中 oos.writeObject(s); // 释放资源 oos.close(); } ``` ### 读端 —— ObjectInputStreamTest 反序列化 ```java // 来源:course/ObjectInputStreamTest.java public static void main(String[] args) throws IOException, ClassNotFoundException { // 创建反序列化对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/studentObject.txt")); // 调用方法进行反序列化 Object o = ois.readObject(); System.out.println(o); // 输出 Student{...},依赖重写的 toString() } ``` ### 写读闭环流程 ``` 1、创建 Student 对象(内存) 2、new ObjectOutputStream(文件字节流) → writeObject(s) → 序列化到 D:/studentObject.txt 3、new ObjectInputStream(文件字节流) → readObject() → 反序列化回内存 4、System.out.println(o) → 调用重写的 toString() 打印对象内容 ``` > **要点**: > - **写端注意**:`new Student(1001, "张三", "男")` 是演示代码,当前 Student 的构造器是 `Student(Integer, String)` 两个参数,演示时需匹配(代码已注释保留) > - **读端返回 Object**:`readObject()` 返回 `Object` 类型,实际使用时需**强转**成 Student:`Student s = (Student) ois.readObject();` > - **两类异常**:写端抛 `IOException`,读端还需抛 `ClassNotFoundException`(readObject 找不到类时抛出) > - **输出依赖 toString()**:`System.out.println(o)` 打印对象会调用 Student 重写的 `toString()`,得到 `Student{id=1001, name='张三', age=null}` 这样的可读格式 --- ## 14. 课后作业回顾:接口化文件复制效率对比器(homework0807/Demo_03) ### 作业背景 `homework0807/Demo_03.java` 是 8 月 7 日课后作业第三题(接口化文件复制效率对比器),把 8 月 7 日学过的"接口化设计 + 字节缓冲流"做了一次综合应用,与本日的"扩展流(处理流)"体系呼应。 ### ① 复制器接口:统一复制规范 ```java // 来源:homework0807/Demo_03.java interface CopyFile { String copy(String src, String desc) throws IOException; } ``` ### ② 普通字节流实现 Copy01:FileInputStream + FileOutputStream + byte[8192] + try-catch-finally ```java // 来源:homework0807/Demo_03.java static class Copy01 implements CopyFile { @Override public String copy(String src, String desc) { FileOutputStream fos = null; FileInputStream fis = null; try { fos = new FileOutputStream(desc); fis = new FileInputStream(src); byte[] bytes = new byte[8192]; int len; while ((len = fis.read(bytes)) != -1) { fos.write(bytes, 0, len); } } catch (FileNotFoundException e) { throw new RuntimeException(e); } catch (IOException e) { return "文件复制失败"; } finally { try { fos.close(); fis.close(); } catch (IOException e) { throw new RuntimeException(e); } } return "文件复制成功"; } } ``` ### ③ 缓冲流实现 BufferedCopy:BufferedOutputStream / BufferedInputStream 包装底层流 ```java // 来源:homework0807/Demo_03.java static class BufferedCopy implements CopyFile { @Override public String copy(String src, String desc) throws IOException { // 先创建文件流,再用缓冲流包装 FileOutputStream fos = new FileOutputStream(desc); FileInputStream fis = new FileInputStream(src); BufferedOutputStream bos = new BufferedOutputStream(fos); BufferedInputStream bis = new BufferedInputStream(fis); byte[] bytes = new byte[8192]; int len; while ((len = bis.read(bytes)) != -1) { bos.write(bytes, 0, len); } // 先关缓冲流再关底层流 bos.close(); bis.close(); fos.close(); fis.close(); return "复制成功"; } } ``` ### ④ main —— System.currentTimeMillis() 耗时对比 ```java // 来源:homework0807/Demo_03.java public static void main(String[] args) throws IOException { String src = "D:\\CentOS7\\kyl-CentOS7.zip"; // 换成任意大于 10MB 的文件 // 普通字节流复制 long start1 = System.currentTimeMillis(); new Copy01().copy(src, "D:/copy1.zip"); long end1 = System.currentTimeMillis(); System.out.println("耗时:" + (end1 - start1)); // 缓冲流复制 long start2 = System.currentTimeMillis(); new BufferedCopy().copy(src, "D:/copy2.zip"); long end2 = System.currentTimeMillis(); System.out.println("耗时:" + (end2 - start2)); // TODO 10: 输出两次耗时,对比哪个更快 } ``` > **要点**: > - **接口 + 两个实现**:CopyFile 接口定义规范,Copy01(普通字节流)与 BufferedCopy(缓冲流)两种实现,main 中分别 new 并调用——面向接口编程 / 多态复用(回顾 08-07 知识点) > - **异常处理两种风格对照**:Copy01 用 try-catch-finally **内部消化**并返回提示字符串;BufferedCopy 直接 `throws IOException` **向上抛出**——接口允许两种风格并存 > - **缓冲流包装流程**:先建文件流 → 缓冲流包装 → 操作缓冲流 → 先关缓冲流再关底层流(复习 08-07) > - **耗时对比**:`System.currentTimeMillis()` 前后相减,用**同一个大文件**对比两种复制实现的效率 > - **接口成员默认 public**:`interface CopyFile { ... }` 内嵌套定义在类中,方法默认 `public abstract` --- ## 15. 多线程入门:进程、线程、并行与并发、main 主线程(Demo04 与 Hello) ### 概念 —— 什么是进程、什么是线程 代码更新后,`c260808` 新增了**多线程**主题。首先理解三个基础概念:**程序、进程、线程**。 ```java // 来源:course/Demo04.java(注释部分,本次更新新增) /* 什么线程?什么叫进程? 我们知道,程序本质上就是一种文件,它是存储在硬盘中,程序想要被执行,就需要被加载到内存中执行。 在内存中执行的着的程序就是进程。 一个程序运行起来后,就成了进程,需要占用CPU、内存、网络、GPU、磁盘等等.... 一个进程中可能包含多个线程。 比如:360安全卫士,同时进行木马查杀、电脑清理、系统修复、优化加速,每使用一个功能,就是启动一个线程。 */ ``` | 概念 | 说明 | |------|------| | **程序** | 本质是存储在**硬盘**中的文件,是一段静态的代码 | | **进程** | **在内存中正在执行的程序**;占用 CPU、内存、网络、GPU、磁盘等资源 | | **线程** | **进程中的单个顺序控制流**,是一条执行路径;一个进程可能包含多个线程 | > **要点**: > - 一个程序运行起来后就成了**进程**,需要占用各种硬件资源(CPU / 内存 / 网络 / GPU / 磁盘) > - 一个**进程**中可能包含**多个线程**——例如 360 安全卫士同时进行木马查杀、电脑清理、系统修复、优化加速,每个功能就是一个线程 > - **进程三大特性**:**独立性**(能独立运行、系统分配资源和调度的独立单位)、**动态性**(进程本质是程序的一次执行过程,动态产生、动态消亡)、**并发性**(任何进程都可以和其他进程一起并发执行) > - **单线程 vs 多线程**:一个进程只有一条路径就是**单线程程序**;有多条路径就是**多线程程序** > - **main 主线程**:`main` 方法被称为主方法,也就是 Java 程序的**主线程**(`Hello.java` 中的 `main` 就是主线程的入口) ### 并行 vs 并发 ```java // 来源:course/Demo04.java(注释部分) /* 线程的并行和并发 并行:同一个时刻,有多个指令在多个CPU上同时执行。比如:饭店后厨,有3个灶台被三位厨师使用炒菜。 并发:同一时刻,有多个指令在单个CPU上交替执行。比如:饭店后厨,有3个灶台,被一位厨师炒菜。 */ ``` | 概念 | 含义 | 后厨类比 | |------|------|----------| | **并行(Parallel)** | 同一个时刻,多个指令在**多个 CPU** 上**同时执行** | 3 个灶台被 3 位厨师同时使用炒菜 | | **并发(Concurrent)** | 同一时刻,多个指令在**单个 CPU** 上**交替执行** | 3 个灶台被 1 位厨师轮番炒菜 | > **要点**: > - **并行靠硬件**:需要多个 CPU 核心同时工作,是"真·同时" > - **并发靠调度**:单核 CPU 在极短时间内快速切换执行多个线程,宏观上"像同时在跑",微观上是交替执行 > - **多线程的意义**:从软硬件上实现多个线程并发执行,充分利用 CPU,提升性能——这也解释了为什么"多线程 ≠ 一定快"(单核场景下是并发切换) ### 补充 —— CPU 与 GPU 的分工(Demo04 注解) `Demo04` 还顺带对比了 CPU / GPU 的特性差异,帮助理解"多核 / 多线程"的硬件背景: ```java // 来源:course/Demo04.java(注释部分) /* CPU 计算机核心 红色警戒 没有GPU,全靠CPU 特性 适合进行复杂运算,但是不适合进行简单的重复性大量运算 GPU 显卡核心 比特币挖矿 CPU不适合挖矿,比特币本质上就是一种计算机运算, 运算难度不高,复杂程度也不高,但是量大。 3A游戏,只狼、荒野大镖客、GTA5、MC AI 大模型--->GPU 本地部署AI大模型,也是吃GPU */ ``` > **要点**: > - **CPU**(计算机核心):适合**复杂运算**,但不适合**简单、重复、量大**的运算(如挖矿) > - **GPU**(显卡核心):适合**大量简单并行**运算——比特币挖矿、3A 游戏渲染、本地部署 AI 大模型都吃 GPU > - 这个对比呼应"多线程"的性能本质:**不同特性的任务交给不同核心处理**,才能发挥硬件的并发能力 --- ## 16. 实现多线程方式一:继承 Thread 类,重写 run()(MyThread 与 ThreadTest01) ### 概念 —— Java 多线程的第一种实现方式 `Demo04` 给出了 Java 实现多线程的**方式一:继承 `Thread` 类,重写 `run()` 方法**,共 4 个步骤: ```java // 来源:course/Demo04.java(注释部分) /* Java中实现多线程的方式: 方式一: 继承 Thread 类,重写 run()方法。 实现步骤: 1、自定义一个类,继承Thread类 2、在自定义类中重写父类的run()方法,设置在线程中要执行的操作。 3、创建自定义线程类的对象 4、调用自定义线程类的start()方法 启动线程。 */ ``` ### run() 方法 vs start() 方法的区别 ```java // 来源:course/Demo04.java(注释部分) /* 为什么要重写 run()方法? 因为run()方法是用来封装被线程执行的代码。 run()方法和start()方法有什么区别? run()方法是用来封装被线程执行的代码,如果直接调用run(),不会创建新的线程,而是和普通方法一样的调用。 start()方法是启动线程的方法,调用这个方法后会启动一个新的线程,然后在线程中由JVM调用这个线程的run()方法 */ ``` | 方法 | 作用 | 关键区别 | |------|------|----------| | `run()` | **封装**被线程执行的代码(重写它来设置线程要执行的操作) | 直接调用 `run()` **不会创建新线程**,和普通方法调用一样(还是在当前线程执行) | | `start()` | **启动线程**的方法 | 调用 `start()` 会**启动一个新线程**,然后由 JVM 在新线程中调用该线程的 `run()` | > **要点**: > - **为什么重写 run()**:`run()` 用来封装"线程启动后要执行的代码",所以自定义线程类的核心就是重写 `run()` > - **启动线程必须用 start()**:`start()` 启动新线程后由 **JVM 自动调用 run()**;直接调 `run()` 只是普通方法调用,**不产生新线程** > - **构造器传名**:`MyThread` 调用 `super(name)` 把线程名传给 `Thread` 类,之后用 `this.getName()` 获取当前线程名,便于区分多个线程 ### 自定义线程类 —— MyThread `MyThread` 继承 `Thread` 类,重写 `run()` 方法,在 `run()` 中封装"求和 + 打印"的操作: ```java // 来源:course/MyThread.java(本次更新新增文件) public class MyThread extends Thread { public MyThread(String name) { super(name); // 把线程名传给父类 Thread } /** * 重写run方法,这个方法里面的内容,就是线程启动后具体执行的操作。 */ @Override public void run() { int sum = 0; for (int i = 0; i < 10000; i++) { System.out.println(this.getName() + "--->" + i); sum += i; } System.out.println(this.getName() + "--->" + sum); System.out.println(this.getName() + "--->" + "hello world"); System.out.println(this.getName() + "--->" + "java....Thread....我的自定义线程...."); } } ``` ### 启动线程 —— ThreadTest01 `ThreadTest01`(原 `ThreadTest`,本次已重命名)创建两个自定义线程对象(线程1、线程2),调用 `start()` 启动: ```java // 来源:course/ThreadTest01.java(本次更新由 ThreadTest 重命名而来) public class ThreadTest01 { public static void main(String[] args) { // 创建自定义线程类的对象 MyThread mt1 = new MyThread("线程1"); MyThread mt2 = new MyThread("线程2"); // 调用自定义线程类的start()方法 启动线程。 mt1.start(); mt2.start(); } } ``` > **要点**: > - **启动两个线程**:`mt1.start()` / `mt2.start()` 分别启动"线程1"、"线程2"两条执行路径,两条路径的 `run()` **交替并发执行**(不是等线程1跑完再跑线程2) > - **getName() 区分线程**:两个线程对象名不同,`this.getName()` 打印出 "线程1" / "线程2",可以直观看到两个线程的输出**交错出现** > - **可反复运行观察**:每次运行两个线程的执行顺序可能不同(由 CPU 调度决定),这正是"多线程并发"的特征 --- ## 17. 实现多线程方式二:实现 Runnable 接口(RunnableImpl 与 ThreadTest02) ### 概念 —— Java 多线程的第二种实现方式(更推荐) 代码更新后,`Demo04` 补充了**方式二:实现 `Runnable` 接口**——相比方式一(继承 Thread),方式二**更推荐**: ```java // 来源:course/Demo04.java(注释部分,本次更新新增) /* 方式二:实现Runnable接口 更推荐这种 Thread类的构造方法: public Thread(Runnable task) 创建一个线程类对象,传入Runnable接口的实现类对象。 public Thread(Runnable task, String name)创建一个线程类对象,传入Runnable接口的实现类对象。并且指定线程的名称 Runnable接口中有一个run()抽象方法,必须要实现类重写。 实现步骤: 1、定义一个实现类实现Runnable接口 2、在实现类中重写run()方法,指定线程的任务。 3、创建实现类对象 4、创建Thread类的对象,把实现类对象作为参数传入Thread的构造方法 5、调用Thread类的start()方法启动线程 方式二相比于方式一,更推荐,因为可以实现多个接口,并且可以继承其他类,不会改变程序类的上下文语义。 */ ``` ### Thread 类的构造方法(接收 Runnable) ```java // 来源:course/Demo04.java(注释部分) public Thread(Runnable task) // 创建线程对象,传入 Runnable 接口实现类对象 public Thread(Runnable task, String name)// 创建线程对象,传入 Runnable 实现类对象,并指定线程名称 ``` ### 方式二 vs 方式一 —— 为什么更推荐 | 对比维度 | 方式一:继承 Thread | 方式二:实现 Runnable | |----------|---------------------|----------------------| | 继承关系 | 必须继承 `Thread`,**无法再继承其他类**(Java 单继承) | 实现 `Runnable` **接口**,**不影响继承其他类** | | 接口数量 | 已占用唯一的继承机会 | 还可以**实现多个接口** | | 程序语义 | 类与线程强耦合,类"本身就是一个线程" | 类与线程**解耦**——类是"任务",线程由 Thread 承载 | | 推荐程度 | 基础入门 | **更推荐**:灵活、符合"组合优于继承"思想 | > **要点**: > - **Runnable 接口只有一个 run() 抽象方法**,实现类必须重写它来指定"线程要执行的任务" > - **Thread 构造方法接收 Runnable**:`new Thread(runnableImpl, "线程名")` —— 把"任务对象"传入线程,由 Thread 负责启动 > - **推荐原因**:方式二实现了"任务"与"线程"的**解耦**——类只负责定义任务(实现 Runnable),至于开几个线程、用什么线程都由外部 Thread 决定;同时避免了 Java **单继承**的限制 ### 实现类 —— RunnableImpl `RunnableImpl` 实现 `Runnable` 接口,在 `run()` 中打印"熊大熊二光头强"并循环输出当前线程名: ```java // 来源:course/RunnableImpl.java(本次更新新增文件) public class RunnableImpl implements Runnable { @Override public void run() { System.out.println("熊大熊二光头强"); // 获取当前线程的名字 String name = Thread.currentThread().getName(); for (int i = 0; i < 1000; i++) { System.out.println("我是" + name + "--->" + i); } } } ``` ### 启动线程 —— ThreadTest02(三种提供 Runnable 的方式 + 主线程演示) `ThreadTest02` 演示了**三种**把任务交给线程的方式:实现类对象、**匿名内部类**、**Lambda 表达式**;启动三个子线程后,**main 主线程也参与循环打印**(本次更新新增),直观看到 4 条执行路径并发: ```java // 来源:course/ThreadTest02.java(本次更新新增,并补充主线程演示) public class ThreadTest02 { public static void main(String[] args) { // 方式一:创建实现类对象,传入 Thread 构造方法 RunnableImpl ri = new RunnableImpl(); Thread t1 = new Thread(ri, "光头强线程!"); // 方式二:匿名内部类(Runnable 只有一个抽象方法 run(),可用匿名内部类) Thread t2 = new Thread(new Runnable() { @Override public void run() { String name = Thread.currentThread().getName(); for (int i = 0; i < 1000; i++) { System.out.println("我是" + name + "--->" + i); } } }, "熊大线程!"); // 方式三:Lambda 表达式(Runnable 是函数式接口,可用 Lambda 简化匿名内部类) Thread t3 = new Thread(() -> { String name = Thread.currentThread().getName(); for (int i = 0; i < 1000; i++) { System.out.println("我是" + name + "--->" + i); } }, "熊二线程!"); t1.start();// 执行了这个方法,才会创建一个新的线程 t2.start(); t3.start(); // 主线程 main 也参与循环打印(本次更新新增)——主线程与三个子线程并发执行 String name = Thread.currentThread().getName(); for (int i = 0; i < 10000; i++) { System.out.println("我是" + name + "--->" + i); } } } ``` > **要点**: > - **三种提供 Runnable 的方式**:①实现类对象 `new Thread(ri, "光头强线程!")`;②匿名内部类 `new Thread(new Runnable(){...}, ...)`;③Lambda 表达式 `new Thread(() -> {...}, ...)`——第三种最简洁,因为 Runnable 是**函数式接口**(只有一个抽象方法) > - **`Thread.currentThread().getName()`**:方式二不继承 Thread,不能直接 `this.getName()`(this 是 Runnable 实现类不是线程),所以用静态方法 `Thread.currentThread()` 拿到**当前正在执行的线程对象**,再调 `getName()` 获取线程名 > - **构造方法指定线程名**:`new Thread(task, name)` 的第二个参数就是线程名,启动后 `Thread.currentThread().getName()` 会返回它(如 "光头强线程!") > - **三个子线程并发执行**:t1 / t2 / t3 的 `start()` 启动三条执行路径,输出会**交错出现** > - **主线程也参与**:`start()` 之后 main 方法继续往下执行(不阻塞),`Thread.currentThread().getName()` 返回 `main`——**主线程与三个子线程共 4 条执行路径并发**,控制台输出交错混排(本次更新新增) --- ## 18. 实现多线程方式三:实现 Callable 接口 + FutureTask(CallableImpl 与 ThreadTest03) ### 概念 —— Java 多线程的第三种实现方式(可返回结果) 前两种方式(继承 Thread / 实现 Runnable)的 `run()` 方法**没有返回值**。如果需要线程计算完成后**返回结果**,就用**方式三:实现 `Callable` 接口**——它的 `call()` 方法**带返回值**,配合 `FutureTask` 使用: ```java // 来源:course/Demo04.java(注释部分,本次更新新增) /* 方式三:实现Callable接口,重写带有返回值的线程操作方法 call() 计算结果,如果无法计算结果,就会抛出一个异常。 FutureTask(Callable callable) 创建一个FutureTask对象,一旦运行就执行参数中的callable对象 V get() 如果有必要,等待计算完成,然后获取计算的结果。 实现步骤: 1、自定义一个类实现Callable接口,并且可以指定泛型,这个泛型其实就是返回值的类型 2、自定义实现类中重写Callable接口的call()方法 3、创建自定义实现类的对象 4、创建FutureTask对象,并且把自定义实现类对象作为参数传入构造方法。 5、创建Thread类的对象,把FutureTask对象作为参数传入构造方法 6、调用Thread类的对象的start()方法,启动线程 7、可以调用get方法,获取线程结束后的结果。 */ ``` ### 核心类与方法 | 成员 | 说明 | |------|------| | `Callable` 接口 | 泛型接口,`V` 是 **call() 的返回值类型**;实现类必须重写 `call()` | | `V call()` | **带返回值**的线程任务方法;无法计算结果时抛异常 | | `FutureTask(Callable callable)` | 包装 Callable 的任务对象,一旦运行就执行其中的 callable | | `V get()` | 等待计算完成,然后**获取计算结果** | > **要点**: > - **Callable vs Runnable 的本质区别**:`Runnable.run()` **无返回值**,`Callable.call()` **有返回值**(泛型指定返回类型),且 `call()` 声明可抛异常 > - **FutureTask 是"中间人"**:Callable 不能直接交给 Thread(Thread 构造只收 Runnable/FutureTask),所以用 `FutureTask` 包装 Callable,再把 FutureTask 传给 Thread > - **get() 阻塞等待**:`get()` 会等待该线程计算完成再返回结果——它拿到的就是对应线程 `call()` 的返回值 ### 带返回值的实现类 —— CallableImpl `CallableImpl` 实现 `Callable`(泛型指定返回类型为 Integer),重写 `call()` 计算 1~100000 的和并返回: ```java // 来源:course/CallableImpl.java(本次更新新增文件) public class CallableImpl implements Callable { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 100000; i++) { sum += i; } String name = Thread.currentThread().getName(); System.out.println(name + "---任务中-->" + sum); return sum; // 返回值就是线程的计算结果 } } ``` ### 启动线程并获取结果 —— ThreadTest03 `ThreadTest03` 用同一个 CallableImpl 创建 3 个 FutureTask,启动 3 个线程,最后用 `get()` 拿到每个线程的计算结果: ```java // 来源:course/ThreadTest03.java(本次更新新增文件) public class ThreadTest03 { public static void main(String[] args) throws ExecutionException, InterruptedException { // 1、创建自定义实现类的对象 CallableImpl ci = new CallableImpl(); // 2、创建FutureTask对象,把实现类对象作为参数传入构造方法 FutureTask ft1 = new FutureTask<>(ci); FutureTask ft2 = new FutureTask<>(ci); FutureTask ft3 = new FutureTask<>(ci); // 3、创建Thread对象,把FutureTask对象作为参数传入构造方法 Thread t1 = new Thread(ft1, "线程1"); Thread t2 = new Thread(ft2, "线程2"); Thread t3 = new Thread(ft3, "线程3"); // 4、start() 启动线程 t2.start(); t3.start(); t1.start(); // 5、调用get()获取线程结束后的结果 Thread.sleep(100); // 主线程休眠,让子线程先跑 Integer i1 = ft1.get(); System.out.println(t1.getName() + "---->" + i1); Integer i2 = ft2.get(); System.out.println(t2.getName() + "---->" + i2); Integer i3 = ft3.get(); System.out.println(t3.getName() + "---->" + i3); } } ``` > **要点**: > - **同一个 Callable 三个线程**:`ci` 被包装进 3 个 FutureTask(ft1/ft2/ft3),分别给 3 个线程——每个线程独立计算一份求和(`FutureTask` 是任务容器,不是线程) > - **get() 获取返回值**:`ft1.get()` 返回线程1 中 `call()` 的 `sum` 值,打印为 "线程1---->4999950000" 之类的结果 > - **Thread.sleep(100)**:主线程休眠 100 毫秒,给子线程留出执行时间,保证 `get()` 之前任务大体已跑完(`get()` 本身也会等待) > - **两种异常**:main 声明 `throws ExecutionException, InterruptedException`——`get()` 抛 ExecutionException(任务内部异常),`sleep()` 抛 InterruptedException ### 三种实现方式对比(Demo04 更新) ```java // 来源:course/Demo04.java(注释部分,本次更新新增) /* 三种实现方式的对比: 实现Runnable、Callable接口 好处:扩展性更强,实现这个接口的同时也可以实现其他接口以及继承其他父类。 缺点:编程相对更复杂,不能直接使用Thread类中的方法 继承Thread类 好处:编程比较简单,可以直接使用Thread类中的方法 缺点:可扩展性比较差,不能再继承其他类。 */ ``` | 维度 | 继承 Thread | 实现 Runnable | 实现 Callable | |------|------------|--------------|---------------| | 任务返回结果 | `run()` 无返回值 | `run()` 无返回值 | `call()` **有返回值** | | 扩展性 | 差(单继承占位,不能再继承其他类) | **强**(可多实现接口 + 可继承父类) | **强**(同 Runnable) | | 编程难度 | 简单 | 较复杂 | 较复杂(需 FutureTask 包装) | | 直接使用 Thread 方法 | 可以(this 就是线程) | 不可以(用 `Thread.currentThread()`) | 不可以 | | 推荐程度 | 入门 | 更推荐 | 需要返回结果时用 | > **要点**: > - **接口派优势**:实现 Runnable / Callable 接口**扩展性更强**——实现这个接口的同时还可以实现其他接口、继承其他父类;缺点是不能直接用 Thread 类的方法(需 `Thread.currentThread()`) > - **继承派优势**:继承 Thread 类**编程更简单**、可以直接使用 Thread 类的方法;缺点是**可扩展性差**,不能再继承其他类(Java 单继承) > - **选型建议**:默认用实现接口(Runnable);需要线程**返回计算结果**时用 Callable + FutureTask ### Thread 类的其他常用方法(Demo04 更新) ```java // 来源:course/Demo04.java(注释部分,本次更新新增) /* Thread类的其他常用的方法: setName(String name) 为这个线程设置线程名 String getName() 获取这个线程的线程名 Thread currentThread() 返回当前正在执行的线程对象的引用 */ ``` > **要点**: > - **`setName(String)` / `getName()`**:实例方法,设置 / 获取**当前线程对象**的线程名(等价于构造方法传 name 的方式) > - **`Thread.currentThread()`**:**静态方法**,返回**当前正在执行这段代码的线程对象**的引用——在实现 Runnable / Callable 接口时(this 不是线程),用它拿线程对象再调 `getName()` 等实例方法 --- ## 19. 知识点全景总结 本课知识点围绕"**字符集转码 + 对象持久化 + 多线程入门**"展开,一张表看清"知识 → 代码位置": | 知识点 | 具体体现 | 源码位置 | |--------|----------|----------| | **编码 / 解码模型** | 字符--编码-->字节--解码-->字符;A 编码 + B 解码 = 乱码 | course/Demo01 注释 | | **转换流概念** | 把 A 字符集的文件转换为 B 字符集的文件(如 GBK 追加内容 → 先转 UTF-8) | course/Demo01 注释 | | **InputStreamReader** | 字节流→字符流桥梁(父类 Reader);读取字节按指定字符集**解码**为字符 | course/Demo01 | | **OutputStreamWriter** | 字符流→字节流桥梁(父类 Writer);读取字符按指定字符集**编码**为字节 | course/Demo01 | | **转换流构造方法** | `InputStreamReader(InputStream)` / `(InputStream, String charsetName)`;OutputStreamWriter 同理,可指定字符集名称 | course/Demo01 注释 | | **转码综合案例** | UTF-8 读 `abc.txt` → GBK 写 `hello.txt`:`new InputStreamReader(new FileInputStream(...), "UTF-8")` + `new OutputStreamWriter(new FileOutputStream(...), "GBK")` + char[1024] 循环 | course/Demo01 main | | **对象持久化概念** | 把内存中对象保存到磁盘 / 网络中传输;字节序列含对象类型、数据、属性 | course/Demo02 注释 | | **对象序列化流 ObjectOutputStream** | `public ObjectOutputStream(OutputStream out)` + `writeObject(Object)` 写入对象 | course/Demo02、ObjectOutputStreamTest | | **对象反序列化流 ObjectInputStream** | `public ObjectInputStream(InputStream in)` + `readObject()` 返回 Object(需强转) | course/Demo02、ObjectInputStreamTest | | **Serializable 标识接口** | 无方法无属性,仅作"可被序列化"的标识;类必须实现才能序列化 | course/Demo02 注释、Student | | **serialVersionUID 版本兼容** | `private static final long serialVersionUID = 42L;` 固定版本号;类属性变化后反序列化不报 InvalidClassException,缺的属性赋默认值 | course/Demo02 注释、Student | | **serialVersionUID 三大作用** | ①验证反序列化版本一致性 ②未声明时 JVM 按类结构自动生成 ③类结构变→自动版本号变→InvalidClassException;最佳实践显式声明(值无所谓,两端一致即可) | course/Demo02 注释 | | **InvalidClassException** | 类结构变化后旧序列化数据反序列化直接报错 | course/Demo02 注释 | | **transient 跳过序列化** | `private transient Integer id;` 该属性不参与序列化,反序列化为默认值(null) | course/Demo02 注释、Student | | **序列化注意事项(6 条)** | 静态属性不序列化;transient 不序列化;父类 Serializable 子类自动序列化;反序列化不调用构造方法;引用链所有对象必须可序列化;单例需重写 readResolve() | course/Demo02 注释 | | **静态属性不参与序列化** | static 成员属于类、不属于对象,天然不参与序列化 | course/Demo02 注释 | | **父类 Serializable → 子类自动序列化** | 继承体系中父类实现 Serializable,子类无需重复实现 | course/Demo02 注释 | | **反序列化不调用构造方法** | readObject() 按字节序列在内存中重构对象,不走构造方法 / 初始化块 | course/Demo02 注释 | | **引用链必须全部可序列化** | 对象引用其他对象时,引用链上所有对象必须实现 Serializable,否则抛 NotSerializableException | course/Demo02 注释 | | **单例 readResolve()** | 反序列化绕过构造器会破坏单例;重写 readResolve() 返回既有单例保持全局唯一 | course/Demo02 注释 | | **Student 实体类** | implements Serializable + serialVersionUID + transient + equals/hashCode/toString 重写(Objects 工具类) | course/Student | | **序列化写端演示** | 创建对象 → ObjectOutputStream 包装文件流 → writeObject(s) → close | course/ObjectOutputStreamTest | | **反序列化读端演示** | ObjectInputStream 包装文件流 → readObject() → println 输出(调用 toString) | course/ObjectInputStreamTest | | **课后作业:接口化复制** | CopyFile 接口 + Copy01(普通字节流 try-catch-finally)+ BufferedCopy(缓冲流 throws)+ currentTimeMillis 耗时对比 | homework0807/Demo_03 | | **扩展流(处理流)体系** | 缓冲流(08-07)+ 转换流 + 对象流均属于"处理流":包装底层字节流,增加缓冲 / 转码 / 序列化能力(Demo03 完整汇总) | course/Demo01、Demo02、Demo03 | | **IO 流体系总结(Demo03)** | 基础流(字节流 InputStream/OutputStream、字符流 Reader/Writer)+ 处理流(缓冲/转换/对象流);常用 write/read/flush/close;特殊 readLine/newLine/writeObject/readObject;特殊属性 serialVersionUID;特殊关键字 transient | course/Demo03 | | **多线程概念** | 程序是硬盘中的文件;内存中执行的程序是进程;进程中的单个顺序控制流是线程;main 方法是主线程 | course/Demo04、Hello | | **进程三大特性** | 独立性(独立运行 / 资源分配调度单位)、动态性(一次执行过程,动态产生消亡)、并发性(可与其他进程并发执行) | course/Demo04 注释 | | **并行 vs 并发** | 并行:同一时刻多指令在多个 CPU 上同时执行;并发:同一时刻多指令在单个 CPU 上交替执行(后厨灶台类比) | course/Demo04 注释 | | **CPU vs GPU** | CPU 适合复杂运算;GPU 适合简单重复量大的并行运算(挖矿 / 3A 游戏 / AI 大模型) | course/Demo04 注释 | | **继承 Thread 实现多线程** | ①自定义类继承 Thread ②重写 run() 封装线程操作 ③创建线程对象 ④调 start() 启动 | course/Demo04 注释、MyThread | | **run() vs start()** | run() 封装被线程执行的代码,直接调用不创建新线程(普通方法);start() 启动新线程,由 JVM 调用该线程的 run() | course/Demo04 注释 | | **构造器传线程名** | `MyThread(String name) { super(name); }` 把线程名传给 Thread,`this.getName()` 获取线程名 | course/MyThread | | **启动多线程演示(方式一)** | ThreadTest01(原 ThreadTest)创建线程1 / 线程2 两个线程对象,分别 start(),run() 交替并发执行(输出交错) | course/ThreadTest01 | | **实现 Runnable 接口(方式二)** | ①实现类实现 Runnable ②重写 run() ③创建实现类对象 ④new Thread(实现类对象, 线程名) ⑤start() 启动;比方式一更推荐(不占继承位、可多实现接口、任务与线程解耦) | course/Demo04 注释、RunnableImpl | | **Thread 构造方法接收 Runnable** | `Thread(Runnable task)` / `Thread(Runnable task, String name)` —— 传入任务对象并可指定线程名 | course/Demo04 注释 | | **Thread.currentThread().getName()** | 方式二不继承 Thread,用静态方法 Thread.currentThread() 获取当前线程对象再取线程名 | course/RunnableImpl、ThreadTest02 | | **匿名内部类提供 Runnable** | `new Thread(new Runnable(){ ... }, "熊大线程!")` —— Runnable 可用匿名内部类实现 | course/ThreadTest02 | | **Lambda 提供 Runnable** | `new Thread(() -> { ... }, "熊二线程!")` —— Runnable 是函数式接口,Lambda 最简洁 | course/ThreadTest02 | | **三种提供任务方式对比** | 实现类对象(最传统)/ 匿名内部类(临时任务)/ Lambda(函数式接口最简洁) | course/ThreadTest02 | | **实现 Callable 接口(方式三)** | ①类实现 Callable\ 指定返回类型 ②重写 call() ③创建对象 ④new FutureTask\(callable) ⑤new Thread(futureTask) ⑥start() ⑦get() 获取结果;call() 带返回值、可抛异常 | course/Demo04 注释、CallableImpl | | **FutureTask 包装 Callable** | `FutureTask(Callable callable)` 包装 Callable 任务,传给 Thread 构造;`V get()` 等待计算完成获取结果 | course/ThreadTest03 | | **get() 获取线程结果** | `ft.get()` 阻塞等待该线程 call() 计算完成,返回结果;抛 ExecutionException / InterruptedException | course/ThreadTest03 | | **三种实现方式对比** | 继承 Thread:简单但扩展性差(占单继承位);实现 Runnable/Callable:扩展性强(可多实现接口 + 可继承父类)但编程复杂、不能直接用 Thread 方法 | course/Demo04 注释 | | **Thread 常用方法** | `setName(String)` 设线程名 / `getName()` 取线程名 / 静态 `Thread.currentThread()` 返回当前执行线程对象 | course/Demo04 注释、RunnableImpl、CallableImpl | | **主线程参与并发** | ThreadTest02 中 start() 后 main 继续执行循环打印,主线程与 3 个子线程共 4 条路径并发(输出交错) | course/ThreadTest02 | --- ## 20. 随堂练习要点 - **转换流转码练习**:参考 `Demo01` main,把 `D:/abc.txt`(UTF-8)转码为 GBK 的 `D:/hello.txt`,再用 `InputStreamReader(...,"GBK")` 读回验证内容正确;改错字符集(如读端写 UTF-8)观察乱码现象 - **指定字符集读写练习**:用 `InputStreamReader` + `OutputStreamWriter` 完成"读 GBK 文件 → 写 UTF-8 文件",体会两个构造参数 `String charsetName` 的作用 - **字符流 vs 转换流对照**:上一日 `FileReader/FileWriter` 只能使用默认字符集;改用 `InputStreamReader/OutputStreamWriter` 手动指定字符集,对比异同,理解"FileReader = 字节流 + 默认字符集" - **序列化基础练习**:仿照 `ObjectOutputStreamTest` 写端(取消注释),把 `Student` 对象序列化到 `D:/studentObject.txt`,再运行 `ObjectInputStreamTest` 反序列化读回,观察控制台输出 - **transient 验证练习**:序列化后再反序列化,观察 `Student` 的 `id` 输出是否为 `null`(因为 id 被 transient 修饰),验证"transient 属性不参与序列化" - **serialVersionUID 练习**:序列化 Student 对象后,修改 Student 类(如注释/新增属性 sex),再反序列化:先去掉 serialVersionUID 看是否报 `InvalidClassException`,再加上 serialVersionUID 对比验证兼容效果 - **readObject 强转练习**:把 `ObjectInputStreamTest` 中 `Object o = ois.readObject()` 改为 `Student s = (Student) ois.readObject();`,强转后调用 `s.getName()` 等 getter 方法 - **对象数组 / 多个对象序列化**:尝试连续 `writeObject()` 序列化多个 Student 对象,再连续 `readObject()` 读回,体会字节序列的顺序性 - **序列化注意事项验证**:①给 Student 加一个 `static` 字段,序列化 / 反序列化后验证其值不受影响(静态属性不参与序列化);②让 Student 引用一个未实现 Serializable 的类(如 Address),序列化观察 `NotSerializableException` 指向引用链上哪个类;③在构造方法中加打印语句,反序列化时观察是否触发(验证"反序列化不调用构造方法") - **父类序列化练习**:定义一个实现 Serializable 的父类和一个未显式声明的子类,直接序列化子类对象,验证"父类实现 → 子类自动可序列化" - **单例 readResolve() 练习**:给单例类实现 Serializable,序列化后再反序列化,打印两次对象 hashCode 对比是否为同一实例;重写 `readResolve()` 返回既有单例后再次验证 - **IO 流体系梳理练习**:仿照 `Demo03` 小结,自己画出完整 IO 流体系图(基础流:字节 / 字符流;处理流:缓冲流 / 转换流 / 对象流),并说出每个流"处理什么、怎么配对包装、有什么特有方法" - **课后作业复习**:运行 `homework0807/Demo_03`,用同一个大文件对比 Copy01(普通字节流)与 BufferedCopy(缓冲流)的复制耗时,验证缓冲流效率更高;再把 Copy01 的 try-catch-finally 与 BufferedCopy 的 throws 两种异常处理风格对比 - **继承 Thread 创建线程**:仿照 `MyThread` 自定义一个线程类(继承 Thread + 重写 run()),在 run() 中循环打印 0~999;创建两个线程对象并 `start()` 启动,观察两个线程的输出**交替出现**,体会并发执行 - **run() 与 start() 对比实验**:分别用 `mt.run()` 和 `mt.start()` 启动同一个线程,观察输出区别——直接调 `run()` 时只有一个线程顺序执行(先执行完 run 再走下一步),而 `start()` 启动的是独立线程 - **多线程求和实验**:在 run() 中做 1~10000 求和并打印,启动多个线程,观察每个线程的求和结果是否正确(当前代码无共享数据,各线程独立计算) - **主线程实验**:在 `main` 中先启动两个子线程再打印 "main 主线程" 内容,观察"主线程输出"与"子线程输出"的先后顺序(由 CPU 调度决定,顺序不定) - **概念梳理**:用"程序 / 进程 / 线程"三个词解释:Java 源文件(.java)、运行中的 JVM、main 方法与 start() 出来的子线程,各自属于哪个概念 - **实现 Runnable 接口**:仿照 `RunnableImpl` 定义一个实现类(implements Runnable + 重写 run()),创建两个 `Thread` 对象并指定不同线程名,`start()` 启动,观察两个线程输出交错;用 `Thread.currentThread().getName()` 打印当前线程名 - **三种提供 Runnable 的方式**:在 `ThreadTest02` 中分别用**实现类对象、匿名内部类、Lambda 表达式**创建三个线程(光头强 / 熊大 / 熊二),体会三种写法从繁琐到简洁的演进,重点理解 Lambda 为什么能替代匿名内部类(Runnable 是函数式接口) - **方式一 vs 方式二对比**:分别用继承 Thread 和实现 Runnable 写一个"打印 1~100"的线程任务,对比两种写法的类结构;思考为什么方式二更推荐(提示:Java 单继承、接口多实现、任务与线程解耦) - **线程名实验**:用 `new Thread(task, "自定义名")` 指定线程名后,在 run() 中用 `Thread.currentThread().getName()` 打印,验证拿到的是构造时指定的名字;不指定名字时观察默认线程名(如 Thread-0 / Thread-1) - **改造练习**:把 `MyThread`(继承 Thread 方式)改造成"实现 Runnable 方式"的 `MyRunnable`,再在 ThreadTest02 中用实现类对象方式启动,体会两种方式代码结构的不同 - **实现 Callable 接口**:仿照 `CallableImpl` 定义一个实现 `Callable` 的类,重写 `call()` 计算 1~N 的和并返回;用 `FutureTask` 包装后启动线程,`get()` 打印返回值 - **get() 等待机制实验**:在 `call()` 中加 `Thread.sleep()` 模拟耗时计算,观察 `get()` 调用前主线程是否被阻塞(get 会等待计算完成);比较"调用 get() 前打印"和"调用 get() 后打印"的输出顺序 - **三种方式完整对比**:分别用继承 Thread、实现 Runnable、实现 Callable 各写一个"求 1~100 的和"的线程任务,对比三种代码结构(有无返回值、能否继承其他类、能否直接用 getName()) - **主线程并发实验**:运行更新后的 `ThreadTest02`,观察控制台是否同时出现 "main"、"光头强线程!"、"熊大线程!"、"熊二线程!" 四类输出交错混排,验证主线程与子线程并发执行 - **Thread 常用方法练习**:用 `setName("新名字")` 动态修改线程名,再用 `getName()` 打印验证;在 main 中调用 `Thread.currentThread().getName()` 确认主线程名是 "main" --- ## 21. 拓展阅读 - **JDK 文档**:[`java.io.InputStreamReader`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/InputStreamReader.html)、[`java.io.OutputStreamWriter`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/OutputStreamWriter.html)、[`java.io.ObjectOutputStream`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/ObjectOutputStream.html)、[`java.io.ObjectInputStream`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/ObjectInputStream.html)、[`java.io.Serializable`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/Serializable.html) - **字符集与乱码**:文件的"保存编码"与"读取编码"必须一致;`FileReader` / `FileWriter` 只能使用默认字符集,而 `InputStreamReader` / `OutputStreamWriter` 可以手动指定字符集,是处理跨字符集文件的利器;常用的 `UTF-8` 是 `Unicode` 的一种可变长实现,兼容 ASCII - **装饰器模式**:转换流、缓冲流、对象流都是"装饰器模式"在 Java I/O 中的体现——用对象包装对象、一层套一层(`ObjectOutputStream(BufferedOutputStream(FileOutputStream(...)))`),每层各加一种能力(缓冲 / 转码 / 序列化) - **标记接口(Marker Interface)**:`Serializable`、`Cloneable` 等接口没有任何方法,仅作为"能力标识";`instanceof Serializable` 判断对象是否可序列化 - **serialVersionUID 规范**:强烈建议所有实现 `Serializable` 的类**显式声明** `private static final long serialVersionUID = 1L;`,否则类一旦修改结构,JVM 自动生成版本号变化会导致反序列化抛 `InvalidClassException` - **transient 与 static**:`static` 修饰的成员属于类、不属于对象,**本身就不参与序列化**;`transient` 用于让实例成员也跳过序列化 - **序列化与继承**:父类实现 `Serializable` 则子类自动可序列化;若父类**未实现** Serializable,则父类部分字段不参与序列化,且反序列化时会调用父类的**无参构造器**(对比:子类部分反序列化不调用构造方法) - **NotSerializableException**:引用链上任一对象未实现 `Serializable` 就会抛此异常,异常信息会**点名是哪个类**——排查时顺着引用链逐级检查 - **readResolve() 与单例保护**:反序列化是"绕过构造器创建新实例"的旁门左道;`readResolve()` 是单例模式对抗序列化的标准手段——在 readResolve() 中返回内存中既有的单例实例,保证全局唯一 - **Externalizable**:除 `Serializable` 外还有 `Externalizable` 接口,实现 `writeExternal()` / `readExternal()` 可**完全自定义**序列化内容(比 Serializable 更可控但需手动实现) - **网络传输对象**:对象持久化不只用于本地磁盘文件,还广泛用于 **RMI、RPC、消息队列** 等场景——对象跨网络传输的本质也是"对象 → 字节序列 → 网络 → 字节序列 → 对象" - **更现代的序列化方案**:Java 原生序列化写出的文件是二进制、且不够高效;生产环境常改用 **JSON(Jackson / Gson / Fastjson)**、**XML** 等文本格式,或 **Protobuf** 等二进制协议做对象持久化 / 传输 - **IO 流学习路线**:File(路径管理)→ 字节流 → 字节缓冲流 → 字符流 → 字符缓冲流 → **转换流(字符集转码)** → **对象流(对象持久化 / 序列化)**,后面还可学打印流(PrintStream / PrintWriter)、数据流(DataInputStream / DataOutputStream)等 - **JDK 文档**:[`java.lang.Thread`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html)(线程类,`run()` / `start()` / `getName()` 等核心方法) - **多线程实现方式**:本日学了**方式一(继承 Thread)**与**方式二(实现 Runnable 接口)**;后续还有**方式三(实现 Callable 接口 + FutureTask,可返回结果)**、**线程池(ExecutorService)** 等——面向接口实现 Runnable 更推荐,因为 Java 是单继承,实现接口可以保留继承其他类的机会 - **Runnable 的函数式编程**:`Runnable` 是**函数式接口**(只有一个抽象方法 run()),所以可以用 **Lambda 表达式** `new Thread(() -> {...})` 或**方法引用**简化匿名内部类——这是 08-04 学过的函数式编程在并发编程中的应用 - **Thread.currentThread()**:方式二中没有继承 Thread,无法直接 `this.getName()`;`Thread.currentThread()` 是静态方法,返回**当前正在执行该行代码的线程对象**,配合 `getName()` / `setName()` / `getPriority()` 使用 - **方式一 vs 方式二**:方式一"继承 Thread"类本身就是线程(强耦合);方式二"实现 Runnable"把**任务**与**线程**解耦——同一个 Runnable 对象可以交给多个 Thread 执行(共享任务),是官方更推荐的做法 - **共享任务的线程安全问题**:同一个 Runnable 对象交给多个线程执行时,run() 中如果有**共享成员变量**就会产生数据竞争(如多线程卖票票数自减);后续将用 `synchronized` 等机制解决 - **JDK 文档**:[`java.util.concurrent.Callable`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Callable.html)、[`java.util.concurrent.FutureTask`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/FutureTask.html)、[`java.util.concurrent.Future`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/Future.html) - **Future 接口**:`FutureTask` 实现了 `Future` 接口——`get()` 获取结果、`isDone()` 判断任务是否完成、`cancel()` 取消任务;`Future` 就是"异步计算的结果占位符",体现了**异步编程**思想 - **Callable 的线程池应用**:`ExecutorService` 线程池的 `submit(Callable)` 方法直接返回 `Future` 对象,是生产环境"提交带返回值的任务"的主流写法(比手动 new Thread + FutureTask 更规范) - **三种方式到线程池**:继承 Thread(直接 new)、实现 Runnable(配合 Executor)、实现 Callable(submit 返回 Future)——学习路线最终都会走向**线程池**统一管理线程(复用、控制并发数、避免频繁创建销毁线程的开销) - **线程生命周期**:线程有新建(New)→ 就绪(Runnable)→ 运行(Running)→ 阻塞(Blocked)→ 死亡(Terminated)等状态;`start()` 只是让线程进入就绪,具体运行由 CPU 调度 - **线程安全问题**:多个线程同时访问**共享数据**时会出现数据不一致(如两个线程同时给同一个变量自增);解决手段有 `synchronized` 同步代码块 / 同步方法、`Lock` 锁、`volatile` 等(后续课程学习) - **线程常用方法**:`sleep(毫秒)`(让线程休眠)、`join()`(等待线程执行完)、`yield()`(让出 CPU)、`setName/getName`(设置/获取线程名)、`isAlive()`(判断线程是否存活)等 - **并发编程参考**:JDK 提供了 `java.util.concurrent` 并发包(`ThreadPoolExecutor`、`ConcurrentHashMap`、`CountDownLatch` 等),是生产环境多线程编程的主流工具