Java 学习笔记

基于项目代码和注释整理,涵盖 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 的转换

  • charint 可以相互转换
  • 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

子类对父类中继承过来的方法进行重新定义。

以下修饰的方法不能被重写staticprivatefinal

抽象类 abstract

  • 可以定义抽象方法
  • 不能直接实例化对象
  • 抽象类有构造方法(接口没有)
abstract class EasyBabstract extends EasyBParent implements IEasyBChild {}

向上转型与向下转型

Object obj = new EasyB();        // 向上转型(父类引用指向子类对象)
EasyB easyB = (EasyB) obj;       // 向下转型(强制类型转换)

类型安全保障方式

  1. 类型检查 — 获取类对象进行比较
  2. 验证实例instanceof 关键字
  3. 泛型
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-catchthrows
// 运行时异常 — 不强制处理
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 对 ByteShortIntegerLongCharacterBoolean 的值做了缓存(-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

存值流程

  1. 根据 key 的 hashCode() 计算存储位置
  2. 该位置无数据 → 将 key-value 封装成 Node 存入数组
  3. 该位置有数据 → 遍历链表,key 相同则覆盖
  4. 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 中
  • 不重复
  • 数据结构:红黑树

排序比较规则

  1. 如果构造时传入了 Comparator(比较器),优先使用比较器的 compare()
  2. 否则使用元素自身的 compareTo()(需要实现 Comparable 接口)
  3. 如果元素既不是 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.javasrc/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) 时,会创建多个实例,违反单例原则。

解决方案

  1. synchronized 关键字
  2. 双重检查锁(DCL)
  3. 静态内部类方式
  4. 枚举方式

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 → 死锁,程序卡住。

避免死锁的方法

  1. 所有线程按相同顺序获取锁
  2. 使用 tryLock() 设置超时时间
  3. 减少锁的嵌套

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. Thread26. Runnable
synchronized / Lock 28. 线程安全
死锁 31. 死锁
wait/notify 32. 线程通信
生产者消费者 33. 生产者-消费者
Callable / FutureTask 34. Callable
上一篇