基于项目代码和注释整理,涵盖 Java 基础语法到多线程编程的核心知识点。
目录
- 第一部分:Java 基础语法
- 1. 数据类型与类型转换
- 2. 运算符
- 3. 面向对象基础
- 4. Object 类与对象创建
- 5. 异常处理
- 6. 内部类
- 7. 多态与属性/方法调用规则
- 8. String 字符串
- 9. String 常用方法
- 10. 基本类型封装类与装箱拆箱
- 11. 集合框架 — List
- 12. 集合框架 — Set
- 13. 集合框架 — Map 与 HashMap 原理
- 14. HashMap 底层原理验证(反射)
- 15. Map 实现类对比
- 16. TreeSet 与排序
- 17. 泛型
- 18. IO — File 类
- 19. IO — 字节流
- 20. IO — 字符流
- 21. IO — 文件复制与 try-with-resources
- 22. IO — 缓冲流与转换流
- 23. IO — 对象流与序列化
- 24. 网络编程 — Socket 聊天室
- 第二部分:Java 多线程
第一部分:Java 基础语法
1. 数据类型与类型转换
对应源码:
src/com/EasyA.java
Java 运行过程
编码 -> 编译 -> 运行
隐式转换(自动类型提升)
取值范围 小 -> 大 自动转换,无需手动处理:
int a = 10;
long b = a; // int -> long
float c = b; // long -> float(float 比 long 取值范围大)
取值范围排序:byte < short < int < long < float < double
强制类型转换
取值范围 大 -> 小 需要显式转换,可能丢失精度:
int a = 128;
byte cd = (byte) a;
System.out.println(cd); // -128(溢出)
char 与 int 的转换
char和int可以相互转换char不能存储负数,范围�(0) ~(65535)
各类型的默认值
| 类型 | 默认值 | |
|---|---|---|
| 整数类型 | 0 |
|
| 浮点类型 | 0.0 |
|
| 字符类型 | � |
|
| 布尔类型 | false |
|
| 引用类型 | null |
boolean类型不能与其他类型相互转换。
2. 运算符
对应源码:
src/com/EasyA.java
数学运算符 + - * / %
- 整数间的数学运算结果 一定是
int类型;有long参与则结果为long a % b结果的正负取决于a的正负
byte bc = 12, bd = 22;
byte be = (byte)(bc + bd); // 需要强制转换,因为 bc+bd 的结果是 int
关系运算符 > < >= <= == !=
==比较基本数据类型时比较的是值==比较引用数据类型时比较的是地址
System.out.println(0 == 0.0); // true(基本类型比较值)
逻辑运算符 && || !
短路现象:&& 左侧为 false 时,右侧不执行。
System.out.println(a++ < 5 && a++ > 5); // 短路:左侧 false 则右侧不执行
位运算符 & | ^ << >> >>>
| 运算符 | 说明 |
|---|---|
& |
按位与 — 都转为二进制,右侧对齐,上下比较 |
| |
按位或 |
^ |
按位异或 — 不同为 1,相同为 0 |
<< |
左移 — 右侧补 0(等价于 * 2^n) |
>> |
右移 — 补符号位(正数补 0,负数补 1) |
>>> |
无符号右移 — 始终补 0 |
System.out.println(8 >> 1); // 4(等价于 8/2)
System.out.println(8 << 2); // 32(等价于 8*4)
位运算是 Java 中最快的数学运算。
一元运算符 ++ --
a++(后置):先取值,后自增++a(前置):先自增,后取值
a = 12;
a = a++; // a++ 返回 12(旧值),然后 a 变为 13,最后 a = 12(赋值覆盖)
赋值运算符 = += -= /= %=
byte ab = 10;
ab += 10; // 正确(自动处理类型转换)
// ab = ab + 10; // 编译错误(结果是 int,不能赋值给 byte)
三元运算符
a = 2 == 2 ? 111 : 222; // 111
// 类型提升:12 是 int,2.3+4 是 double,结果为 double -> 12.0
System.out.println(2 == 2 ? 12 : 2.3 + 4); // 12.0
3. 面向对象基础
对应源码:
src/com/EasyB.java
继承 extends
- 类可以继承类,接口可以继承接口
super关键字用于调用父类的属性和方法
class EasyBChild extends EasyBParent {
public void testChild() {
super.test(); // 调用父类方法
super.age = 10; // 访问父类属性
}
}
重写 @Override
子类对父类中继承过来的方法进行重新定义。
以下修饰的方法不能被重写:
static、private、final
抽象类 abstract
- 可以定义抽象方法
- 不能直接实例化对象
- 抽象类有构造方法(接口没有)
abstract class EasyBabstract extends EasyBParent implements IEasyBChild {}
向上转型与向下转型
Object obj = new EasyB(); // 向上转型(父类引用指向子类对象)
EasyB easyB = (EasyB) obj; // 向下转型(强制类型转换)
类型安全保障方式
- 类型检查 — 获取类对象进行比较
- 验证实例 —
instanceof关键字 - 泛型
if (obj instanceof EasyB) { ... }
多态
| 类型 | 说明 | 阶段 |
|---|---|---|
| 动态多态 | 方法重写,运行时才能确定调用哪个对象的方法 | 运行期 |
| 静态多态 | 方法重载,编译时就确定调用哪个方法 | 编译期 |
4. Object 类与对象创建
对应源码:
src/com/EasyD.java
clone 克隆
EeayD1 easyD1 = new EeayD1();
Object cloneObj = easyD1.clone();
System.out.println(cloneObj == easyD1); // false — 克隆出来的是新对象
Java 中创建对象的方式
| 方式 | 说明 |
|---|---|
new |
最常用 |
clone() |
实现 Cloneable 接口,浅拷贝 |
| 序列化 | 实现 Serializable 接口(见后文) |
5. 异常处理
对应源码:
src/com/EasyE.java
异常体系
Throwable
├── Exception(可处理)
│ ├── RuntimeException(运行时异常,不强制处理)
│ └── 编译异常 / 检查性异常(必须处理)
└── Error(程序无法干预,如 OOM)
try-catch-finally
try {
c = str.charAt(20);
} catch (StringIndexOutOfBoundsException e) {
e.printStackTrace();
c = str.charAt(0);
} finally {
System.out.println("finally"); // 无论是否异常都会执行
}
自定义异常
- 继承
RuntimeException:运行时异常,调用方可以不处理 - 继承
Exception:检查型异常,调用方必须try-catch或throws
// 运行时异常 — 不强制处理
class PrinceTooHighException extends RuntimeException {
public PrinceTooHighException(String message) { super(message); }
}
// 检查型异常 — 必须声明 throws 或 try-catch
class CountTooManyException extends Exception {
public CountTooManyException(String message) { super(message); }
}
throw vs throws
throw:在方法内部抛出异常对象throws:在方法签名上声明可能抛出的异常类型
6. 内部类
对应源码:
src/com/EasyF.java
四种内部类
| 类型 | 定义位置 | 创建方式 |
|---|---|---|
| 成员内部类 | 类的成员位置 | outer.new Inner() |
| 静态内部类 | 类的成员位置,static 修饰 |
new Outer.Inner() |
| 局部内部类 | 方法内部 | 直接 new |
| 匿名内部类 | 使用时定义 | new Interface() { ... } |
public class EasyF {
class AA {} // 成员内部类
static class BB {} // 静态内部类
public void test() {
class CC {} // 局部内部类
}
public static void main(String[] args) {
EasyF easyF = new EasyF();
EasyF.AA aa = easyF.new AA(); // 成员内部类需要依赖外部类实例
EasyF.BB bb = new EasyF.BB(); // 静态内部类不需要
}
}
7. 多态与属性/方法调用规则
对应源码:
src/com/EasyG.java
核心规则
- 调用属性看声明类型(编译时确定)
- 调用方法看实际对象类型(运行时确定)
EasyG2 easyG2 = new EasyG2();
System.out.println(easyG2.age); // 22(声明类型 EasyG2,找 EasyG2 的 age)
EasyG1 easyG1 = easyG2; // 向上转型
System.out.println(easyG1.age); // 11(声明类型 EasyG1,找 EasyG1 的 age)
easyG2.test(); // G2(实际对象是 EasyG2)
easyG1.test(); // G2(实际对象还是 EasyG2,方法看对象)
8. String 字符串
对应源码:
src/com/EasyH.java
String 的特性
- 字符串是常量,定义后不会变化
- 字符串拼接
+从左到右计算
System.out.println(1 + 2 + "3"); // "33"(先算 1+2=3,再拼接 "3")
System.out.println("3" + 1 + 2); // "312"(字符串拼接从左到右)
字符串常量池
工作原理:声明字符串时,JVM 检查常量池中是否有相同字符串:
- 有 → 直接返回引用
- 没有 → 创建对象放入常量池,返回引用
"1234" == "1234" // true(常量池中同一对象)
"12" + "34" == "1234" // true(编译期常量折叠)
new String("1234") == "1234" // false(new 在堆上创建新对象)
new String("1234") == new String("1234") // false(两个不同堆对象)
编译期常量优化
String S1 = "12", S2 = "34";
System.out.println(S1 + S2 == "1234"); // false(S1、S2 是变量)
final String S11 = "12", S22 = "34";
System.out.println(S11 + S22 == "1234"); // true(final 变量编译期确定)
intern() 方法
将字符串加入常量池:
- 常量池中已存在 → 返回常量池中的引用
- 常量池中不存在 → 创建副本放入常量池,返回该副本
new String("123").intern() == new String("123").intern() // true
当两个字符串
equals()为true时,它们的intern()的==一定为true。
== vs equals()
| 比较方式 | 基本类型 | 引用类型 |
|---|---|---|
== |
比较值 | 比较地址 |
equals() |
N/A | 比较内容(默认比较地址,String 重写为比较内容) |
9. String 常用方法
对应源码:
src/com/EasyI.java
| 方法 | 说明 |
|---|---|
charAt(index) |
获取指定下标的字符 |
indexOf(str) |
子串首次出现的下标,不存在返回 -1 |
lastIndexOf(str) |
子串最后出现的下标 |
length() |
字符串长度(数组用 arr.length 属性) |
substring(begin, end) |
截取子串,含 begin 不含 end |
replace(old, new) |
替换所有匹配子串,返回新字符串(原串不变) |
replaceAll(regex, new) |
正则替换 |
split(regex) |
分割字符串;切首字符会出现空串,切末字符不会 |
getBytes() |
转为 byte[] |
10. 基本类型封装类与装箱拆箱
对应源码:
src/com/EasyJ.java
对应关系
| 基本类型 | 封装类 |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
装箱与拆箱
- 装箱:基本类型 → 封装类型对象
- 拆箱:封装类型 → 基本类型值
int i5 = 10;
Integer i3 = 10; // 自动装箱
int i6 = i3; // 自动拆箱
缓存机制(-128 ~ 127)
Java 对 Byte、Short、Integer、Long、Character、Boolean 的值做了缓存(-128 ~ 127):
Integer i1 = 12, i2 = 12;
System.out.println(i1 == i2); // true(缓存范围内,同一对象)
i1 = 128; i2 = 128;
System.out.println(i1 == i2); // false(超出缓存,不同对象)
基本类型和封装类型用
==比较时,封装类型会拆箱后再比较值。
11. 集合框架 — List
对应源码:
src/com/EasyK.java
集合体系概览
Collection(接口)
├── List(有序,可重复)
│ ├── ArrayList — 数组实现,查找快,增删慢
│ ├── LinkedList — 链表实现,增删快,查找慢
│ └── Vector — 数组实现,线程安全
└── Set(无序,不重复)
Map(接口,K-V 结构)
List 基本操作
List list = new ArrayList();
list.add(1); // 添加
list.set(1, 345); // 替换
list.remove(1); // 删除
list.contains("123"); // 判断是否包含
list.size(); // 长度
list.get(0); // 获取元素
遍历方式
// 1. for 循环
for (int i = 0; i < list.size(); i++) { list.get(i); }
// 2. 迭代器
Iterator it = list.iterator();
while (it.hasNext()) { it.next(); }
// 3. foreach(只能读,不能修改集合元素)
for (Object item : list) { System.out.println(item); }
ArrayList 底层
- 底层是数组
- 动态扩容:每次扩容为原来的 1.5 倍
- 删除/插入数据时,后续元素需要位移
12. 集合框架 — Set
对应源码:
src/com/EasyL.java
特点
- 无序:迭代顺序和添加顺序可能不一致
- 不重复:重复元素不会被添加
主要实现类
| 类 | 特点 |
|---|---|
HashSet |
基于 HashMap,允许 null |
TreeSet |
基于红黑树,不允许 null |
LinkedHashSet |
维护插入顺序 |
Set set = new HashSet();
set.add("12");
set.add("1234");
set.add("1234"); // 重复,不会被添加
set.add(null); // HashSet 允许 null
13. 集合框架 — Map 与 HashMap 原理
对应源码:
src/com/EasyM.java
Map 基本操作
Map map = new HashMap();
map.put("username", "zhangsan"); // 存入
map.get("username"); // 获取
map.put("username", "sss"); // 覆盖(key 重复时)
map.remove("age"); // 删除
Map 遍历
// 遍历 key
Set keys = map.keySet();
for (Object key : keys) {
System.out.println(key + ":" + map.get(key));
}
// 遍历 value
Collection values = map.values();
约束
- key 不能重复,可以有
null值 - value 可以重复,可以存
null
HashMap 底层实现原理
数据结构:数组 + 链表 + 红黑树
核心参数:
| 参数 | 值 |
|---|---|
| 初始容量 | 16 |
| 扩容因子(负载因子) | 0.75 |
| 树化阈值(链表→红黑树) | 8 |
| 退树化阈值(红黑树→链表) | 6 |
| 最小树化容量 | 64 |
存值流程:
- 根据 key 的
hashCode()计算存储位置 - 该位置无数据 → 将 key-value 封装成
Node存入数组 - 该位置有数据 → 遍历链表,key 相同则覆盖
- key 不同 → 将新 Node 添加到链表末尾
扩容规则:
- 元素数量 > 数组容量 × 扩容因子(如 16 × 0.75 = 12)→ 扩容为原来的 2 倍
- 链表元素 > 8 但数组容量 < 64 → 优先扩容而非树化
- 链表元素 > 8 且数组容量 >= 64 → 树化(转为红黑树)
14. HashMap 底层原理验证(反射)
对应源码:
src/com/EasyN.java
通过反射验证 HashMap 的内部存储结构:
Class c = HashMap.class;
Field ftable = c.getDeclaredField("table"); // 获取内部数组
ftable.setAccessible(true);
Object[] obj = (Object[]) ftable.get(map); // 获取数组引用
// 计算存储位置:(数组长度 - 1) & hashCode
int index = (obj.length - 1) & item.hashCode();
通过反射遍历链表中的 Node.next 字段,验证元素在数组中的分布情况。
15. Map 实现类对比
对应源码:
src/com/EasyO.java
| 实现类 | 数据结构 | null key | null value | 线程安全 |
|---|---|---|---|---|
HashMap |
数组+链表+红黑树 | 允许 | 允许 | 否 |
TreeMap |
红黑树 | 不允许 | 允许 | 否 |
LinkedHashMap |
数组+链表+红黑树+额外链表 | 允许 | 允许 | 否 |
Hashtable |
数组+链表 | 不允许 | 不允许 | 是 |
ConcurrentHashMap |
分段锁/数组+链表+红黑树 | 不允许 | 不允许 | 是(性能更高) |
LinkedHashMap通过额外维护一个链表来保证迭代有序性。
16. TreeSet 与排序
对应源码:
src/com/EasyP.java
TreeSet 特点
- 底层是 TreeMap,元素存入 TreeMap 的 key 中
- 不重复
- 数据结构:红黑树
排序比较规则
- 如果构造时传入了 Comparator(比较器),优先使用比较器的
compare() - 否则使用元素自身的
compareTo()(需要实现Comparable接口) - 如果元素既不是
Comparable也没有比较器,抛出ClassCastException
TreeSet set = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
});
17. 泛型
对应源码:
src/com/EasyQ.java
作用
确定处理对象的类型,确保类型安全,避免运行时 ClassCastException。
// 不使用泛型 — 类型不安全
List list1 = new ArrayList();
list1.add("hello");
list1.add(1); // 可以添加任意类型
String str = (String) list1.get(1); // 运行时异常!
// 使用泛型 — 编译期类型检查
ArrayList<String> list = new ArrayList<String>();
list.add("hello");
String str1 = list.get(0); // 无需强转
泛型定义
public class EasyQ<E, K, V, T> { // 类级别泛型
public <W> void test(W w) { } // 方法级别泛型
}
18. IO — File 类
对应源码:
src/com/EasyR.java
File 常用操作
| 方法 | 说明 |
|---|---|
exists() |
文件/目录是否存在 |
createNewFile() |
创建文件(需处理 IOException) |
isDirectory() |
是否是目录 |
isFile() |
是否是文件 |
canRead() / canWrite() / canExecute() |
权限检查 |
mkdir() |
创建单级目录(父目录不存在则失败) |
mkdirs() |
创建多级目录(包含父级路径) |
length() |
文件大小(字节) |
listFiles() |
获取目录下所有文件 |
递归遍历目录
public static void readDIR(String path) {
File file = new File(path);
if (!file.exists()) return;
if (file.isFile()) {
System.out.println(file.getAbsoluteFile());
} else {
File[] files = file.listFiles();
for (File f : files) {
readDIR(f.getAbsolutePath()); // 递归
}
}
}
19. IO — 字节流
对应源码:
src/com/EasyS.java
流的分类
| 维度 | 分类 |
|---|---|
| 流动方向 | 输入流(读)、输出流(写)—— 相对于程序而言 |
| 介质 | 字节流(任意文件)、字符流(文本文件) |
| 功能 | 节点流(直接操作数据源)、功能流(包装节点流) |
字节流继承体系
InputStream / OutputStream(抽象类)
├── FileInputStream / FileOutputStream
├── BufferedInputStream / BufferedOutputStream
└── ObjectInputStream / ObjectOutputStream
FileOutputStream 写入文件
FileOutputStream out = new FileOutputStream("D:/easy.txt");
out.write("1234565".getBytes());
out.flush(); // 输出流关闭前必须刷新缓冲区
out.close();
输出流关闭前需要调用
flush()刷新缓冲区。
20. IO — 字符流
对应源码:
src/com/EasyT.java
字符流继承体系
Reader / Writer(抽象类)
├── FileReader / FileWriter
├── BufferedReader / BufferedWriter
└── InputStreamReader / OutputStreamWriter(转换流)
FileReader 读取文件
FileReader fr = new FileReader(file);
char[] chars = new char[4];
int len;
while ((len = fr.read(chars)) != -1) {
System.out.println(new String(chars, 0, len));
}
fr.close();
字符流只能处理文本文件,字节流可以处理任何文件。文本文件包括:
.txt、.html、.css、.js、.java、.xml、.json等。
21. IO — 文件复制与 try-with-resources
对应源码:
src/com/EasyU.java
try-with-resources(自动关闭流)
实现 Closeable 接口的流可以在 try() 中声明,自动关闭:
try (
FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile);
) {
byte[] bytes = new byte[1024];
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
fos.flush();
}
}
22. IO — 缓冲流与转换流
对应源码:
src/com/EasyV.java、src/com/EasyW.java
缓冲流
将数据临时存入一个缓冲数组,数组满了才一次性写出,减少 IO 次数,提高效率。
// 缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(fos);
bos.write("内容".getBytes());
bos.flush();
// 缓冲输入流 + 按行读取
BufferedReader br = new BufferedReader(new FileReader("D://easy.txt"));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
转换流
InputStreamReader:字节输入流 → 字符输入流OutputStreamWriter:字节输出流 → 字符输出流
23. IO — 对象流与序列化
对应源码:
src/com/EasyX.java
序列化与反序列化
- 序列化:对象 → 可传输的数据(字节序列)
- 反序列化:可传输的数据 → 对象
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://easy.txt"));
oos.writeObject(student);
oos.flush();
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://easy.txt"));
Object obj = ois.readObject();
序列化要求
- 被序列化的类必须实现
Serializable接口 serialVersionUID决定反序列化能否成功(版本号不一致则失败)- 如果没有显式定义
serialVersionUID,JVM 会根据类的结构自动生成一个
transient 关键字
修饰的属性不会被序列化:
class Student implements Serializable {
private static final long serialVersionUID = 1L;
private transient String name; // 不会被序列化
private transient int age; // 不会被序列化
private String code; // 会被序列化
}
24. 网络编程 — Socket 聊天室
对应源码:
src/com/EasyY.java(服务器)、src/com/EasyZ.java(客户端)
核心概念
- Socket(套接字):IP + 端口号,标识网络中的一个进程
- ServerSocket:服务端监听连接
- Socket:客户端发起连接
服务端架构(EasyY)
ServerSocket 监听 8866 端口
↓ accept()
每个客户端连接 → 创建 ClientThread 线程处理
↓
维护 SOCKET_LIST(在线客户端列表)
↓
收到消息 → sendMsg() 广播给其他客户端
客户端架构(EasyZ)
连接到服务端 192.168.110.3:8866
↓
启动两个线程:
├── EasyZOutThread:从控制台读取输入 → 发送给服务端
└── EasyZInTread:从服务端接收消息 → 打印到控制台
关键代码:服务端广播
public static void sendMsg(String msg, Socket socket) {
for (Socket s : SOCKET_LIST) {
if (s == socket) continue; // 不给自己发
OutputStream out = s.getOutputStream();
out.write((socket.getInetAddress() + ":" + msg + "n").getBytes());
}
}
第二部分:Java 多线程
25. 线程创建方式一:继承 Thread
对应源码:
src/main/java/test1/
public class ChatThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 11; i++) {
System.out.println("聊天");
}
}
}
// 启动线程
Thread t1 = new Thread(new ChatThread());
t1.start(); // start() 会创建新线程并调用 run()
注意:调用
start()而不是直接调用run()。直接调用run()只是在当前线程中执行方法,不会创建新线程。
26. 线程创建方式二:实现 Runnable + 线程池
对应源码:
src/main/java/test2/
实现 Runnable 接口
public class ChatThread1 implements Runnable {
@Override
public void run() { ... }
}
线程池 ExecutorService
// 创建固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(100);
// 提交任务
executorService.execute(new ChatThread1());
executorService.execute(new EeatThread1());
// 关闭线程池
executorService.shutdown();
匿名内部类写法(最常用)
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程运行");
}
});
Thread vs Runnable 对比
| 方式 | 优点 | 缺点 |
|---|---|---|
继承 Thread |
简单直观 | Java 单继承,不能继承其他类 |
实现 Runnable |
可以继承其他类,适合资源共享 | 需要包装成 Thread 对象 |
27. 线程的命名与获取
对应源码:
src/main/java/test3/
myThread.setName("线程一"); // 设置线程名
Thread.currentThread().setName("主线程"); // 设置当前线程名
System.out.println(Thread.currentThread().getName()); // 获取当前线程名
28. 线程安全 — synchronized 与 ReentrantLock
对应源码:
src/main/java/test4/
问题场景:多窗口卖票
三个窗口共享 100 张票,多线程并发操作共享变量 n 会出现数据不一致。
方式一:synchronized 方法
synchronized public void sale() {
if (n <= 0) return;
System.out.println(Thread.currentThread().getName() + "销售了第" + n + "张票");
n--;
}
方式二:synchronized 代码块
while (true) {
synchronized (this) {
if (n <= 0) break;
System.out.println("销售了第" + n + "张票");
n--;
}
}
方式三:ReentrantLock
ReentrantLock lock = new ReentrantLock();
while (true) {
try {
lock.lock();
if (n <= 0) break;
System.out.println("销售了第" + n + "张票");
n--;
} finally {
lock.unlock(); // 必须在 finally 中释放锁
}
}
对比
| 方式 | 特点 |
|---|---|
synchronized |
JVM 层面的锁,自动获取/释放 |
ReentrantLock |
API 层面的锁,需手动 lock()/unlock(),更灵活 |
29. 线程控制 — join 等待
对应源码:
src/main/java/test5/
join() 的作用
join() 让当前线程等待目标线程执行完成后再继续执行。
// 实现 t1 → t2 → t3 的顺序执行
Thread t2 = new Thread(() -> {
t1.join(); // 等待 t1 完成
System.out.println("t2");
});
Thread t3 = new Thread(() -> {
t2.join(); // 等待 t2 完成
System.out.println("t3");
});
链式等待(MyThread 封装)
MyThread t1 = new MyThread(null); // 无前置依赖
Thread t2 = new MyThread(t1); // 等待 t1
Thread t3 = new MyThread(t2); // 等待 t2
// 输出顺序:t4 → t4 → t4(按依赖链执行)
30. 单例模式的线程安全问题
对应源码:
src/main/java/test6/
线程不安全的懒汉式单例
public class SingeObject {
private static SingeObject obj;
private SingeObject() {}
public static SingeObject getInstance() throws InterruptedException {
if (obj == null) { // 多线程可能同时通过判断
Thread.sleep(100); // 模拟延迟,放大竞态条件
obj = new SingeObject(); // 可能创建多个实例!
}
return obj;
}
}
问题:当多个线程同时进入 if (obj == null) 时,会创建多个实例,违反单例原则。
解决方案:
- 加
synchronized关键字 - 双重检查锁(DCL)
- 静态内部类方式
- 枚举方式
31. 死锁
对应源码:
src/main/java/test7/
死锁产生条件
两个线程以相反的顺序获取两把锁,导致互相等待。
// 线程 A:先锁 m,再锁 n
synchronized (Lock.m) {
synchronized (Lock.n) {
System.out.println("A...");
}
}
// 线程 B:先锁 n,再锁 m
synchronized (Lock.n) {
synchronized (Lock.m) {
System.out.println("B...");
}
}
结果:A 持有 m 等待 n,B 持有 n 等待 m → 死锁,程序卡住。
避免死锁的方法
- 所有线程按相同顺序获取锁
- 使用
tryLock()设置超时时间 - 减少锁的嵌套
32. 线程通信 — wait/notify
对应源码:
src/main/java/test8/
wait/notify 机制
wait():让当前线程释放锁并进入等待状态notify():唤醒一个在该对象上等待的线程notifyAll():唤醒所有在该对象上等待的线程
这三个方法必须在
synchronized代码块中调用。
交替打印数字(两个线程协作)
public class PrintNum implements Runnable {
private Integer i = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
if (i > 20) break;
this.notify(); // 唤醒另一个线程
System.out.println(Thread.currentThread().getName() + ":" + i);
i++;
try {
this.wait(); // 自己等待,释放锁
} catch (InterruptedException e) { ... }
}
}
}
}
执行流程:
线程1: 获得锁 → notify → 打印 → wait(释放锁)
线程2: 获得锁 → notify → 打印 → wait(释放锁)
线程1: 被唤醒 → 获得锁 → notify → 打印 → wait
...循环直到 i > 20
33. 生产者-消费者模式
对应源码:
src/main/java/test9/
共享仓库
public class Store {
public static ArrayList<Object> list = new ArrayList<>(100);
public static final int MAX_NUM = 100;
}
生产者
public class Producer extends Thread {
@Override
public void run() {
while (true) {
synchronized (Store.list) {
if (Store.list.size() < Store.MAX_NUM) {
Store.list.notifyAll(); // 唤醒消费者
Store.list.add(new Object());
System.out.println("生产商品,目前数量:" + Store.list.size());
} else {
System.out.println("仓库已满,停止生产");
Store.list.wait(); // 仓库满,生产者等待
}
}
}
}
}
消费者
public class Consumer extends Thread {
@Override
public void run() {
while (true) {
synchronized (Store.list) {
if (Store.list.size() > 0) {
Store.list.notifyAll(); // 唤醒生产者
Store.list.remove(0);
System.out.println("消费商品,目前数量:" + Store.list.size());
} else {
System.out.println("仓库已空,停止消费");
Store.list.wait(); // 仓库空,消费者等待
}
}
}
}
}
协作模式
仓库未满:生产者生产 → notifyAll 唤醒消费者
仓库已满:生产者 wait → 等待消费者消费
仓库未空:消费者消费 → notifyAll 唤醒生产者
仓库已空:消费者 wait → 等待生产者生产
34. Callable 与 FutureTask
对应源码:
src/main/java/test10/
与 Runnable 的区别
| 特性 | Runnable | Callable |
|---|---|---|
| 返回值 | 无 (void) |
有 (泛型) |
| 异常 | 不能抛出受检异常 | 可以抛出 Exception |
| 方法名 | run() |
call() |
使用方式
// 定义 Callable 任务
public class SumThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Integer sum = 0;
for (int i = 1; i <= 100; i++) { sum += i; }
return sum;
}
}
// 通过 FutureTask 获取返回值
SumThread s1 = new SumThread();
FutureTask<Integer> futureTask = new FutureTask<>(s1);
Thread thread = new Thread(futureTask);
thread.start();
Integer sum = futureTask.get(); // 阻塞等待结果
System.out.println(sum); // 5050
futureTask.get()会阻塞当前线程,直到 Callable 任务执行完成并返回结果。
知识点索引速查
| 主题 | 章节 |
|---|---|
| 类型转换 | 1. 数据类型与类型转换 |
| 位运算 | 2. 运算符 |
| 继承、重写、多态 | 3. 面向对象基础、7. 多态 |
| 异常处理 | 5. 异常处理 |
| String 常量池 | 8. String 字符串 |
| 装箱拆箱 / 缓存 | 10. 封装类 |
| ArrayList / LinkedList | 11. List |
| HashMap 原理 | 13. Map 与 HashMap |
| IO 流 | 19~23. IO |
| 序列化 | 23. 对象流 |
| Socket 网络编程 | 24. 网络编程 |
| 线程创建 | 25. Thread、26. Runnable |
| synchronized / Lock | 28. 线程安全 |
| 死锁 | 31. 死锁 |
| wait/notify | 32. 线程通信 |
| 生产者消费者 | 33. 生产者-消费者 |
| Callable / FutureTask | 34. Callable |