|
|
@@ -0,0 +1,1246 @@
|
|
|
+# 20260808 课堂笔记 — 转换流(InputStreamReader / OutputStreamWriter 字节↔字符桥梁 + 字符集编码解码与乱码成因 + UTF-8 转 GBK 文件转码案例)+ 对象持久化与序列化(对象持久化概念 + Serializable 标识接口 + ObjectOutputStream 序列化 writeObject / ObjectInputStream 反序列化 readObject + serialVersionUID 三大作用与版本兼容 + transient 跳过序列化 + 序列化注意事项(静态属性 / 引用链 / 构造方法 / 单例 readResolve)+ Student 实体类综合演示 + IO 流体系总结(基础流 vs 处理流全景))+ 多线程入门(进程 / 线程 / 并行并发概念 + 方式一继承 Thread / 方式二实现 Runnable / 方式三实现 Callable+FutureTask + 三种实现方式对比 + run() 与 start() 区别 + 匿名内部类与 Lambda 创建 Runnable + main 主线程)+ 课后作业回顾(接口化文件复制效率对比器)
|
|
|
+
|
|
|
+- **日期**: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<V> callable) 创建一个FutureTask对象,一旦运行就执行参数中的callable对象
|
|
|
+ V get() 如果有必要,等待计算完成,然后获取计算的结果。
|
|
|
+ 实现步骤:
|
|
|
+ 1、自定义一个类实现Callable接口,并且可以指定泛型,这个泛型其实就是返回值的类型
|
|
|
+ 2、自定义实现类中重写Callable接口的call()方法
|
|
|
+ 3、创建自定义实现类的对象
|
|
|
+ 4、创建FutureTask对象,并且把自定义实现类对象作为参数传入构造方法。
|
|
|
+ 5、创建Thread类的对象,把FutureTask对象作为参数传入构造方法
|
|
|
+ 6、调用Thread类的对象的start()方法,启动线程
|
|
|
+ 7、可以调用get方法,获取线程结束后的结果。
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+### 核心类与方法
|
|
|
+
|
|
|
+| 成员 | 说明 |
|
|
|
+|------|------|
|
|
|
+| `Callable<V>` 接口 | 泛型接口,`V` 是 **call() 的返回值类型**;实现类必须重写 `call()` |
|
|
|
+| `V call()` | **带返回值**的线程任务方法;无法计算结果时抛异常 |
|
|
|
+| `FutureTask<V>(Callable<V> 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>`(泛型指定返回类型为 Integer),重写 `call()` 计算 1~100000 的和并返回:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/CallableImpl.java(本次更新新增文件)
|
|
|
+public class CallableImpl implements Callable<Integer> {
|
|
|
+ @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<Integer> ft1 = new FutureTask<>(ci);
|
|
|
+ FutureTask<Integer> ft2 = new FutureTask<>(ci);
|
|
|
+ FutureTask<Integer> 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\<V\> 指定返回类型 ②重写 call() ③创建对象 ④new FutureTask\<V\>(callable) ⑤new Thread(futureTask) ⑥start() ⑦get() 获取结果;call() 带返回值、可抛异常 | course/Demo04 注释、CallableImpl |
|
|
|
+| **FutureTask 包装 Callable** | `FutureTask<V>(Callable<V> 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<Integer>` 的类,重写 `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` 等),是生产环境多线程编程的主流工具
|