Java基础知识总结
Java基础知识总结
本文整合了 Java 虚拟机、集合框架、文件IO、反射、注解、代理、线程、锁、并发编程等核心基础知识。
目录
一、Java虚拟机(JVM)
1.1 什么是Java虚拟机
Java 跨平台的原理:Java 语言不直接将代码编译成与系统相关的机器码,而是编译成一种特定的语言规范——字节码(Bytecode)。Java 虚拟机会解析字节码文件并将其翻译为各操作系统能理解的机器码。
JVM 本质上是一个字节码翻译器。

1.2 编译过程
编译器分为三类:
| 编译器类型 | 作用 | 特点 |
|---|---|---|
| 前端编译器(javac) | 源代码 → 字节码 | 将 .java 编译为 .class |
| JIT 编译器(即时编译器) | 字节码 → 机器码 | 运行时编译热点代码,HotSpot 内置 C1(编译快、优化保守)和 C2(编译慢、优化好)两种模式 |
| AOT 编译器(静态提前编译器) | 源代码 → 机器码 | 减少”第一次运行慢”的体验 |
前端编译过程:词法分析/语法分析 → 填充符号表 → 分析注解 → 生成字节码。
1.3 字节码文件结构
字节码文件是一组以 8 位为最小基础的十六进制数据流,由无符号数和表组成。
核心组成部分:
- 魔数:前 4 字节固定为
0xCAFEBABE,用于识别 Class 文件 - 文件版本:第 5-8 字节,包含次版本号和主版本号(如 JDK 1.8 =
0x00000034) - 常量池:存放字面量和符号引用
- 访问标志:标识类或接口的访问信息(public、abstract 等)
- 类索引/父类索引/接口索引:确定类的继承关系
- 字段表:描述类中声明的变量
- 方法表:描述类中的方法
- 属性表:描述类的属性信息
可通过 javap -verbose 类名.class 反编译查看字节码。
1.4 JVM 内存结构
JVM 内存分为公有(线程共享)和私有(线程独有)两部分:
公有部分
Java 堆:几乎所有对象实例都在此分配内存。按对象存活时间分为:
- 新生代(Young Generation):
- Eden 区:新对象的出生地
- SurvivorFrom / SurvivorTo:Minor GC 时,幸存对象在两者之间复制(复制算法)
- 默认比例 Eden : From : To = 8 : 1 : 1
- 老年代(Old Generation):存活时间长的对象,采用标记清除法或标记压缩法
- 方法区/元空间:存放 Class 和 Meta 信息。JDK 8 后用元空间(Metaspace)取代永久代,元空间使用本地内存而非 JVM 内存
私有部分
- PC 寄存器:保存当前线程正在执行的方法地址
- Java 虚拟机栈:存储栈帧,每个栈帧包含局部变量表、操作数栈、动态连接、方法返回地址等信息
- 本地方法栈:管理本地方法(Native Method)的调用
1.5 类加载机制
类加载过程分为七个阶段:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
- 加载:将字节码数据加载到内存
- 验证:校验 Class 文件格式是否符合 JVM 规范
- 准备:为 static 变量分配内存并赋予默认零值(final 修饰的赋实际值)
- 解析:将常量池中的符号引用替换为直接引用
- 初始化:按语句顺序执行类初始化(static 变量赋值、static 代码块),有父类先初始化父类。整体是惰性初始化
- 使用:JVM 从入口方法开始执行程序
- 卸载:销毁 Class 对象,JVM 退出内存
1.6 垃圾回收(GC)
判断对象存活
- 引用计数法(已淘汰):对象被引用时 +1,取消引用时 -1。缺陷:无法解决循环引用
- 可达性分析(GC Root Tracing):从 GC Root 出发,可达的对象存活,不可达的为垃圾
GC Root 主要包括:
- 虚拟机栈中引用的对象
- 类静态属性引用的对象
- 常量引用的对象
- 本地方法栈中 JNI 引用的对象
四种引用类型
| 引用类型 | 回收时机 | 使用场景 |
|---|---|---|
| 强引用(Strong Reference) | 永不回收 | Object obj = new Object() |
| 软引用(Soft Reference) | 内存不足时回收 | 缓存 |
| 弱引用(Weak Reference) | GC 时即回收 | ThreadLocal |
| 虚引用(Phantom Reference) | 任何时候都可能 | 跟踪对象回收通知 |
垃圾回收算法
| 算法 | 过程 | 优缺点 | 适用场景 |
|---|---|---|---|
| 标记清除 | 标记存活对象,清除未标记的 | 会产生内存碎片 | 老年代 |
| 复制算法 | 将存活对象复制到另一块区域,清空当前区域 | 内存利用率只有一半 | 新生代(存活对象少) |
| 标记压缩 | 标记存活对象,移动到内存一端,清空剩余区域 | 无碎片,但耗时 | 老年代 |
GC 术语
- Minor GC / Young GC:从年轻代回收内存
- Major GC / Old GC:从老年代回收内存
- Full GC:清理年轻代、老年代和元空间
- Stop-The-World:GC 时暂停所有用户线程
垃圾回收器
| 回收器 | 类型 | 特点 |
|---|---|---|
| Serial | 串行 | 单线程,Stop-The-World |
| ParNew | 并行(新生代) | Serial 的多线程版本 |
| Parallel Scavenge | 并行(新生代) | 注重吞吐量 |
| Parallel Old | 并行(老年代) | 注重吞吐量 |
| CMS(Concurrent Mark Sweep) | 并发(老年代) | 注重低停顿,工作流程:初始标记→并发标记→并发预清理→重新标记→并发清除 |
| G1(Garbage First) | 并发 | JDK 9+ 默认,分区回收,优先回收价值最大的 Region |
JDK 1.8 默认:Parallel Scavenge(新生代)+ Parallel Old(老年代)
1.7 JVM 常用参数
堆栈配置:
| 参数 | 含义 |
|---|---|
-Xms |
初始堆大小 |
-Xmx |
最大堆空间 |
-Xmn |
新生代大小 |
-XX:SurvivorRatio |
Eden 和 Survivor 的比例 |
-XX:MetaspaceSize |
元空间 GC 阈值(JDK 8+) |
-XX:MaxMetaspaceSize |
最大元空间大小(JDK 8+) |
-Xss |
栈大小 |
跟踪监控:
| 参数 | 含义 |
|---|---|
-XX:+PrintGC |
打印 GC 日志 |
-XX:+PrintGCDetails |
打印详细 GC 日志 |
-verbose:class |
跟踪类的加载和卸载 |
-XX:+TraceClassLoading |
跟踪类的加载 |
常用 JDK 监控工具:
jps:列出所有 Java 进程jstat:监视堆信息(如jstat -gcutil <pid>)jinfo:查看运行中程序的扩展参数jmap:生成 Dump 文件,查看堆内对象统计jhat:分析堆快照jstack:查看线程状态,可检测死锁jconsole:图形化监控工具
二、Java集合框架
2.1 整体架构
Java 集合框架主要分为两大接口体系:
- Collection 接口:单列集合,存储单个元素
- List:有序、可重复 →
ArrayList、LinkedList、Vector - Set:无序、不可重复 →
HashSet、TreeSet、LinkedHashSet - Queue:队列 →
LinkedList、PriorityQueue、ArrayDeque
- List:有序、可重复 →
- Map 接口:双列集合,存储键值对 →
HashMap、TreeMap、LinkedHashMap、Hashtable
2.2 常用集合类
| 集合类 | 底层结构 | 线程安全 | 特点 |
|---|---|---|---|
| ArrayList | 动态数组 | 否 | 查询快,增删慢 |
| LinkedList | 双向链表 | 否 | 增删快,查询慢,可实现队列/栈 |
| HashSet | HashMap | 否 | 无序,元素唯一 |
| TreeSet | 红黑树 | 否 | 有序(自然排序或比较器排序) |
| HashMap | 数组+链表+红黑树(JDK 8+) | 否 | 键值对,key 不重复,支持 null 键 |
| TreeMap | 红黑树 | 否 | key 有序 |
2.3 HashMap 原理
- JDK 7:数组 + 链表,采用头插法,扩容时可能导致循环链表(死链)
- JDK 8:数组 + 链表 + 红黑树,采用尾插法。当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树;当红黑树节点 ≤ 6 时转回链表
- 默认初始容量 16,负载因子 0.75,扩容时容量翻倍
三、Java文件IO
3.1 按字符读写(Reader / Writer)
适用于处理文本文件。
1 | |
配合 BufferedWriter / BufferedReader 可逐行读写:
1 | |
3.2 按字节读写(InputStream / OutputStream)
适用于处理所有类型文件(文本、图片、视频等)。
1 | |
四、Java反射
反射(Reflection)允许程序在运行期获取任意一个对象的所有信息,包括类名、方法、字段等。
4.1 Class 类
- 每加载一个类,JVM 会创建一个唯一的
Class对象与之关联 Class对象包含了.class文件的完整信息.class是懒加载的
获取 Class 对象的三种方式
1 | |
获取类信息
1 | |
4.2 操作字段(Field)
1 | |
4.3 操作方法(Method)
1 | |
4.4 操作构造方法(Constructor)
1 | |
五、Java注解
注解(Annotation)是给代码贴的标签/标记,本身不做任何事,只携带信息,由解析器(反射/AOP/框架)读取并执行逻辑。
5.1 元注解
用于注解其他注解的注解:
| 元注解 | 说明 |
|---|---|
| @Target | 指定注解可以用在什么地方(TYPE、METHOD、FIELD 等) |
| @Retention | 指定注解保留到什么时候:SOURCE(源码阶段)、CLASS(编译阶段)、RUNTIME(运行期,默认) |
| @Documented | 生成 javadoc 时保留注解信息 |
| @Inherited | 使子类能继承父类的注解 |
5.2 自定义注解示例
1 | |
5.3 通过 AOP 解析注解
1 | |
六、Java代理
代理模式:给某一个对象提供一个代理,并由代理对象控制对真实对象的访问。
角色:Subject(接口)、RealSubject(被代理对象)、Proxy(代理对象)
6.1 静态代理 vs 动态代理
| 类型 | 特点 |
|---|---|
| 静态代理 | 运行前确定代理关系,代理类字节码已生成。优点:简单、不侵入原代码。缺点:代理类数量多、修改目标类时需同步修改代理类 |
| 动态代理 | 运行时通过反射动态生成代理类字节码。常见的实现有 JDK 动态代理和 CGLIB 动态代理 |
6.2 JDK 动态代理
基于接口创建代理类:
- 使用
java.lang.reflect.InvocationHandler定义代理逻辑 - 使用
java.lang.reflect.Proxy创建代理对象 - 代理对象通过反射获取方法,然后交给
InvocationHandler执行
6.3 CGLIB 动态代理
基于继承创建代理类:
- 通过生成目标类的子类来实现代理
- 缺点:需要额外引入 CGLIB 包;只能代理非 final 的 public 方法
七、Java线程
7.1 线程生命周期
线程有六种状态:
- NEW:线程正在创建
- RUNNABLE:线程可以运行(可能在等待时间片或正在运行)
- BLOCKED:线程等待获取锁
- WAITING:当前线程等待其他线程唤醒
- TIMED_WAITING:当前线程等待一段时间后自动醒来
- TERMINATED:线程执行完毕或出现异常
7.2 线程创建方式
Java 中有三种创建线程的接口/类:
方式一:继承 Thread 类
1 | |
方式二:实现 Runnable 接口
1 | |
优点:任务与线程解耦,更容易与线程池配合。
方式三:实现 Callable 接口
1 | |
优点:任务可以有返回值,可以抛出异常。
7.3 线程常用 API
| 方法 | 说明 |
|---|---|
Thread.sleep(n) |
当前线程休眠 n 毫秒,进入 TIMED_WAITING 状态,不释放锁 |
Thread.yield() |
建议让出 CPU,由调度器决定 |
thread.join() |
等待调用线程执行完毕后再继续 |
thread.interrupt() |
中断线程:睡眠中的线程会抛 InterruptedException;运行中的线程会设置中断标记 |
thread.isInterrupted() |
检查中断标记(不清除) |
Thread.interrupted() |
检查中断标记(清除) |
thread.setDaemon(true) |
设置为守护线程,主线程结束时守护线程自动终止 |
obj.wait() |
挂起线程,释放锁,只能在 synchronized 块中调用 |
obj.notify() / obj.notifyAll() |
唤醒等待线程,只能在 synchronized 块中调用 |
sleep vs wait 的区别:sleep 抱着锁睡觉(不释放锁),wait 会释放锁。
7.4 线程互斥与协作
互斥方式:
synchronized关键字Lock接口及其实现类
协作方式:
join():等待线程结束wait()/notify()/notifyAll():基于 Object 监视器await()/signal()/signalAll():基于 Condition 条件队列LockSupport.park()/LockSupport.unpark():更底层的阻塞/唤醒
7.5 线程池
为什么使用线程池
对线程进行统一分配、调优和监控,避免频繁创建销毁线程的开销。
ThreadPoolExecutor 核心参数
1 | |
线程池状态
通过原子整数 ctl 的高 3 位记录状态,低 29 位记录线程数:
| 状态 | 接收新任务 | 处理队列任务 |
|---|---|---|
| RUNNING | Y | Y |
| SHUTDOWN | N | Y |
| STOP | N | N |
| TIDYING | - | - |
| TERMINATED | - | - |
常用阻塞队列
| 队列类型 | 特点 |
|---|---|
ArrayBlockingQueue |
基于数组的有界阻塞队列,FIFO |
LinkedBlockingQueue |
基于链表的阻塞队列(可无界),吞吐量较高 |
SynchronousQueue |
不存储元素,插入必须等移除操作 |
PriorityBlockingQueue |
有优先级的无界阻塞队列 |
拒绝策略
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) |
直接抛出 RejectedExecutionException |
CallerRunsPolicy |
由调用者线程执行任务 |
DiscardOldestPolicy |
丢弃队列中最旧的任务,执行新任务 |
DiscardPolicy |
直接丢弃任务 |
Executors 提供的线程池
| 类型 | 特点 | 注意事项 |
|---|---|---|
newFixedThreadPool(n) |
固定大小线程池,无救急线程 | 队列无界,可能导致 OOM |
newCachedThreadPool() |
全是救急线程(60s 存活),遇任务创建 | 线程数无上限,可能导致 OOM |
newSingleThreadExecutor() |
单线程池,任务串行执行 | 故障后会新建线程保证运行 |
newScheduledThreadPool(n) |
支持定时和延时任务 | 任务执行时间长时会延后下一个任务 |
为什么不建议使用 Executors 创建线程池? 因为它使用了无界队列或无限线程数的配置,可能导致内存溢出(OOM)。推荐直接使用
ThreadPoolExecutor构造函数指定具体参数。
八、Java锁
8.1 锁的分类
Java 中的锁分为两类:
- synchronized 关键字 —— JVM 实现,隐式锁,自动获取和释放
- Lock 接口及其实现类 —— 显式锁,需手动
lock()和unlock()
8.2 synchronized
使用方式
1 | |
原理
- 方法级的 synchronized:通过方法标志
ACC_SYNCHRONIZED实现 - 代码块级的 synchronized:通过
monitorenter和monitorexit指令实现 - 每个 Java 对象可以关联一个 Monitor(管程/监视器),Monitor 只能由一个线程拥有
- 其他线程进入 EntryList 阻塞队列等待
- Owner 线程调用
wait()后进入 WaitSet,释放 Monitor
存在的问题
Monitor 的实现依赖操作系统底层的互斥锁(mutex),需要用户态和内核态的切换,效率较低。
8.3 锁升级机制
JVM 通过多级锁优化来解决 synchronized 的性能问题,锁只能升级不能降级:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
| 锁类型 | 原理 | 适用场景 |
|---|---|---|
| 偏向锁 | Mark Word 记录偏向的线程 ID,同一线程重入无需 CAS | 单线程访问 |
| 轻量级锁 | CAS 交换 Mark Word 和锁记录,失败则自旋重试 | 多线程错开访问 |
| 重量级锁 | 依赖 Monitor,线程阻塞 | 多线程竞争激烈 |
- 锁膨胀:轻量级锁 CAS 失败且有竞争时,升级为重量级锁
- 自旋优化:竞争时不立即阻塞,先自旋重试几次(适合多核 CPU)
- 偏向锁撤销:发生竞争时撤销偏向锁。批量重偏向阈值 20 次,批量撤销阈值 40 次
8.4 按性质分类
| 分类维度 | 类型 | 说明 |
|---|---|---|
| 加锁策略 | 悲观锁 | 每次操作都加锁(如 synchronized) |
| 乐观锁 | 通过 CAS 尝试修改,不阻塞(如 AtomicInteger) | |
| 获取顺序 | 公平锁 | FIFO 先来后到 |
| 非公平锁 | 抢占式,synchronized 属于非公平锁 | |
| 持有数量 | 独占锁 | 只能被一个线程持有 |
| 共享锁 | 可被多个线程持有(如读锁) |
8.5 Lock 接口
ReentrantLock
可重入锁,支持公平/非公平(默认非公平),相比于 synchronized 的特点:
- 可中断:
lockInterruptibly() - 可超时:
tryLock(long timeout, TimeUnit unit) - 可设为公平锁:
new ReentrantLock(true) - 支持多个条件变量:
lock.newCondition()创建多个等待队列
1 | |
可重入实现:每次获取锁 state++,释放时只有 state == 0 才真正释放锁。
8.6 AQS(AbstractQueuedSynchronizer)
AQS 是实现锁(ReentrantLock、ReentrantReadWriteLock 等)的基础框架。
核心组成:
- state(int 类型):表示同步状态(0=未锁,1=已锁)
- FIFO 等待队列(CLH 队列):节点分为独占式和共享式
- 条件变量:支持多个等待队列
主要方法:
| 分类 | 方法 | 说明 |
|---|---|---|
| 状态操作 | getState() / setState() / compareAndSetState() |
获取/设置/CAS 设置状态 |
| 独占式 | tryAcquire() / tryRelease() |
子类重写,实现获取/释放逻辑 |
| 共享式 | tryAcquireShared() / tryReleaseShared() |
子类重写,实现共享获取/释放 |
| 模板方法 | acquire() / release() / acquireShared() / releaseShared() |
提供标准流程,调用 tryXxx |
8.7 ReentrantReadWriteLock
读写锁,基于 ReentrantLock 和 AQS 实现:
- 读-读并发,读-写互斥,写-写互斥
- state 高 16 位表示读锁数量,低 16 位表示写锁重入次数
- 有写锁时可直接获取读锁(锁降级);有读锁时不能直接获取写锁
8.8 StampedLock
JDK 8 新增的乐观读写锁,基于”戳”(stamp)判断数据是否被修改:
tryOptimisticRead()获取乐观读戳validate(stamp)校验戳是否通过- 如果校验失败,升级为读锁
注意:StampedLock 不支持条件变量,不支持可重入。适用于读多写少的场景。
九、Java并发编程
9.1 基础概念
| 概念 | 说明 |
|---|---|
| 进程 | 程序的一个实例,资源分配的最小单位 |
| 线程 | 进程内的指令流,CPU 调度的最小单位 |
| 并发 | 单 CPU 交替执行多条指令 |
| 并行 | 多 CPU 同时执行指令 |
| 同步 | 等待结果返回后才继续执行 |
| 异步 | 不等结果,继续执行后续代码 |
9.2 共享问题
临界区与竞态条件
- 临界区:存在对共享资源多线程读写的代码块
- 竞态条件:多线程在临界区内执行,因执行序列不同导致结果无法预测
解决方式:
- 阻塞式:synchronized、Lock
- 非阻塞式:原子变量(基于 CAS)
变量的线程安全
- 成员变量/静态变量:共享且读写 → 线程不安全
- 局部变量:引用常量 → 安全;引用对象逃逸到方法外 → 不安全
9.3 JMM(Java 内存模型)
JMM 定义了主存、工作内存的抽象概念,体现在三个方面:
| 特性 | 说明 | 保证方式 |
|---|---|---|
| 原子性 | 指令不受线程上下文切换影响 | synchronized |
| 可见性 | 一个线程对共享变量的修改对其他线程立即可见 | volatile、synchronized |
| 有序性 | 指令按顺序执行(避免指令重排影响多线程正确性) | volatile |
volatile 关键字
- 保证可见性:每次读取都从主内存获取最新值
- 禁止指令重排:通过内存屏障实现
- 写屏障:在该屏障之前的写操作都同步到主存
- 读屏障:在该屏障之后读取的都是主存最新数据
- 不能保证原子性
9.4 乐观锁与 CAS
CAS(Compare And Swap)
不断尝试比较当前值与预期值,相同则修改,不同则重试:
1 | |
CAS 必须配合 volatile 使用才能读取到共享变量的最新值。
优点:无锁并发,无线程上下文切换开销(适合线程数少、多核 CPU)
缺点:竞争激烈时重试频繁,可能效率低于有锁
ABA 问题
线程 T2 将值 A→B→A,线程 T1 的 CAS 认为值没变,但实际语义已改变。
解决方案:
AtomicStampedReference:加版本号(类似乐观锁)AtomicMarkableReference:只关心是否被修改过(布尔标记)
JUC 原子类
| 类别 | 类名 |
|---|---|
| 原子整数 | AtomicInteger、AtomicLong、AtomicBoolean |
| 原子引用 | AtomicReference、AtomicStampedReference |
| 原子数组 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
| 字段更新器 | AtomicIntegerFieldUpdater、AtomicReferenceFieldUpdater |
| 原子累加器 | LongAdder、DoubleAdder(高并发下性能优于 AtomicLong) |
9.5 ThreadLocal
线程本地变量,每个线程都有变量的独立副本,实现线程隔离。
- 创建:
new ThreadLocal<>() - 存储:
set(value) - 获取:
get() - 清理:
remove()
原理:每个线程有一个 ThreadLocalMap,以 ThreadLocal 的弱引用为 key 存储值。哈希冲突时使用开放定址法解决。
内存泄漏风险:ThreadLocal 的 key 是弱引用,GC 时会被回收,但 value 不会。解决方式:使用完毕后调用
remove()。
9.6 活跃性问题
| 问题 | 说明 |
|---|---|
| 死锁 | 多个线程互相等待对方持有的资源,全部阻塞 |
| 活锁 | 线程互相改变对方的结束条件,都无法结束但仍在运行 |
| 饥饿 | 线程优先级太低,始终得不到 CPU 调度 |
死锁的四个必要条件(破坏任一即可避免):
- 互斥:资源只能给一个线程使用
- 不可剥夺:资源只能由持有者释放
- 请求和保持:持有资源的同时申请其他资源
- 循环等待:形成等待环路
定位死锁:jstack <pid> 会显示 Found one Java-level deadlock:,或用 jconsole 图形化检测。
9.7 DCL(Double-Checked Locking)单例
1 | |
其他单例实现方式:
- 饿汉式:
private static final Singleton INSTANCE = new Singleton();(类加载时创建) - 枚举:
enum Singleton { INSTANCE }(天然单例,防反射、防序列化) - 静态内部类:利用类加载机制的懒汉式,线程安全
9.8 JUC 并发工具
Semaphore(信号量)
限制同时访问共享资源的线程数量:
1 | |
CountDownLatch(倒计数门闩)
等待所有线程完成任务后继续执行(一次性):
1 | |
与 join 相比,CountDownLatch 可以配合线程池使用。
CyclicBarrier(循环栅栏)
与 CountDownLatch 类似,但可以循环使用:
1 | |
ForkJoinPool
JDK 1.7 引入,基于分治思想,适用于 CPU 密集型计算:
- 将大任务拆分为小任务,多线程并行计算,最后合并结果
- 核心类:
RecursiveTask(有返回值)、RecursiveAction(无返回值)
9.9 ConcurrentHashMap
JDK 7 vs JDK 8
| 版本 | 实现 | 问题 |
|---|---|---|
| JDK 7 | Segment 分段锁(继承 ReentrantLock) | 并发度固定 |
| JDK 8 | 数组+链表+红黑树,CAS + synchronized | 更高的并发度 |
JDK 8 原理
- 懒惰初始化:构造时只计算容量,首次使用时才创建数组
- get:无需加锁,使用 volatile 保证可见性
- put:CAS 尝试创建节点/插入头结点,需要时用 synchronized 锁住链表/红黑树
- 扩容:发现其他线程正在扩容(hash 为 -1)时,当前线程会协助扩容
- size 计算:无竞争时累加 baseCount,有竞争时分散到 CounterCell 数组,最后求和
9.10 并发编程模式
保护性暂停(Guarded Suspension)
一个线程等待另一个线程的执行结果(同步模式):
1 | |
使用 while 而非 if 判断条件,防止虚假唤醒问题。