Skip to content

Commit ad1097d

Browse files
committed
docs(java):规范markdown格式
1 parent c060f26 commit ad1097d

16 files changed

Lines changed: 41 additions & 42 deletions

docs/java/basis/java-basic-questions-02.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ public boolean equals(Object anObject) {
534534

535535
`hashCode()` 定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是:`Object``hashCode()` 方法是本地方法,也就是用 C 语言或 C++ 实现的。
536536

537-
> ⚠️ 注意:该方法在 **Oracle OpenJDK8** 中默认是 "使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成", 并不是 "地址" 或者 "地址转换而来", 不同 JDK/VM 可能不同。在 **Oracle OpenJDK8** 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。参考源码:
537+
> ⚠️ 注意:该方法在 **Oracle OpenJDK8** 中默认是 使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成, 并不是 “地址” 或者 地址转换而来, 不同 JDK/VM 可能不同。在 **Oracle OpenJDK8** 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数: -XX:hashCode=4 启用第五种。参考源码:
538538
>
539539
> - <https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/globals.hpp>(1127 行)
540540
> - <https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp>(537 行开始)
@@ -605,7 +605,6 @@ public native int hashCode();
605605

606606
`String` 是不可变的(后面会详细分析原因),每次修改都会生成新的对象,并将引用指向新的实例,而 `StringBuffer``StringBuilder` 都是可变的,它们在修改字符串时不会创建新对象,而是直接在原有字符数组上进行操作。
607607

608-
609608
`StringBuilder``StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串,不过没有使用 `final``private` 关键字修饰,最关键的是这个 `AbstractStringBuilder` 类还提供了很多修改字符串的方法比如 `append` 方法。
610609

611610
```java
@@ -633,10 +632,11 @@ abstract class AbstractStringBuilder implements Appendable, CharSequence {
633632
**性能**
634633

635634
两者的性能差异主要来源于线程安全机制:
635+
636636
- `StringBuffer` 的方法通常是同步的(线程安全),因此会带来一定的性能开销;
637637
- `StringBuilder` 没有同步开销(非线程安全),在单线程场景下通常具有更好的性能表现。
638-
相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
639-
另外,具体的性能差异并不是固定的,在现代 JVM 中由于锁优化(如锁消除),两者在某些场景下性能差距可能较小。
638+
相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
639+
另外,具体的性能差异并不是固定的,在现代 JVM 中由于锁优化(如锁消除),两者在某些场景下性能差距可能较小。
640640

641641
**对于三者使用的总结:**
642642

docs/java/basis/java-basic-questions-03.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ public class DebugInvocationHandler implements InvocationHandler {
396396

397397
## 代理
398398

399-
关于 Java 代理的详细介绍,可以看看笔者写的 [Java 代理模式详解](https://javaguide.cn/java/basis/proxy.html "Java 代理模式详解")这篇文章。
399+
关于 Java 代理的详细介绍,可以看看笔者写的 [Java 代理模式详解](https://javaguide.cn/java/basis/proxy.html Java 代理模式详解)这篇文章。
400400

401401
### 如何实现动态代理?
402402

docs/java/basis/java-keyword-summary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ bar.method2();
252252

253253
总结:
254254

255-
- 在外部调用静态方法时,可以使用类名.方法名”的方式,也可以使用对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
255+
- 在外部调用静态方法时,可以使用类名.方法名”的方式,也可以使用对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
256256
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
257257

258258
### `static{}`静态代码块与`{}`非静态代码块(构造代码块)

docs/java/collection/concurrent-hash-map-source-code.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ public V get(Object key) {
613613

614614
`ConcurrentHashMap` 内部维护了两个关键的计数相关字段:
615615

616-
- **baseCount**:基础计数器,在没有竞争的情况下,直接通过 CAS 更新这个变量。可以把它理解为"主计数器"
616+
- **baseCount**:基础计数器,在没有竞争的情况下,直接通过 CAS 更新这个变量。可以把它理解为主计数器
617617
- **counterCells**:计数器数组。当多个线程竞争 `baseCount` 失败时,会尝试将计数增量分散到 `counterCells` 数组的不同位置。
618618
- 每个线程根据自己的 **Probe 值**(可理解为线程 ID 生成的一种哈希码)映射到数组的某个槽位,优先在这个“偏向的格子”里进行累加。
619619
- **注意**:这个格子并不是严格意义上的“线程私有”,当哈希冲突时,多个线程仍然可能映射到同一个槽位并发更新。

docs/java/concurrent/aqs.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,7 @@ public final boolean hasQueuedPredecessors() {
12551255

12561256
关键原因在于 **减少了线程上下文切换的次数**。当持有锁的线程 A 释放锁后:
12571257

1258-
- **非公平锁**:此时如果恰好有线程 B 正在尝试获取锁(还没有进入同步队列),线程 B 可以直接通过 CAS 获取到锁并立即执行,省去了唤醒队列中线程的开销。而队列中等待的线程被唤醒后发现锁被占用,会重新阻塞,虽然看起来"浪费"了一次唤醒,但总体上减少了线程切换次数。
1258+
- **非公平锁**:此时如果恰好有线程 B 正在尝试获取锁(还没有进入同步队列),线程 B 可以直接通过 CAS 获取到锁并立即执行,省去了唤醒队列中线程的开销。而队列中等待的线程被唤醒后发现锁被占用,会重新阻塞,虽然看起来“浪费”了一次唤醒,但总体上减少了线程切换次数。
12591259
- **公平锁**:线程 B 必须排到队列尾部,然后唤醒队列头部的线程。从线程被唤醒到真正开始执行之间,存在一段 **调度延迟**(线程状态从阻塞切换到运行),在这段延迟期间锁处于空闲状态,降低了锁的利用率。
12601260

12611261
Doug Lea 在 `ReentrantLock` 的文档中指出:使用公平锁的程序在多线程环境下的总体吞吐量通常低于使用非公平锁的程序(即更慢),因此 `ReentrantLock` 默认使用非公平模式。但在需要保证请求处理顺序或避免线程饥饿的场景中(如连接池分配),公平锁是更好的选择。

docs/java/concurrent/java-concurrent-collections.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueu
135135

136136
## ConcurrentSkipListMap
137137

138-
> 下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。
138+
> 下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster 《数据结构与算法之美》)以及《实战 Java 高并发程序设计》。
139139
140140
为了引出 `ConcurrentSkipListMap`,先带着大家简单理解一下跳表。
141141

docs/java/concurrent/java-concurrent-questions-02.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ public native void fullFence();
4848

4949
JMM(Java 内存模型)定义了 4 种内存屏障(Memory Barrier),用于控制特定条件下的指令重排序和内存可见性:
5050

51-
| 屏障类型 | 指令示例 | 说明 |
52-
| --- | --- | --- |
53-
| **LoadLoad** | `Load1; LoadLoad; Load2` | 保证 `Load1` 的读取操作在 `Load2` 及其后续读取操作之前完成 |
54-
| **StoreStore** | `Store1; StoreStore; Store2` | 保证 `Store1` 的写入操作对其他处理器可见(刷新到内存),先于 `Store2` 及其后续写入操作 |
55-
| **LoadStore** | `Load1; LoadStore; Store2` | 保证 `Load1` 的读取操作在 `Store2` 及其后续写入操作刷新到内存之前完成 |
56-
| **StoreLoad** | `Store1; StoreLoad; Load2` | 保证 `Store1` 的写入操作对其他处理器可见,先于 `Load2` 及其后续读取操作。`StoreLoad` 屏障的开销是四种屏障中最大的,它同时具有其他三种屏障的效果,因此也称为 **全能屏障(Full Barrier)** |
51+
| 屏障类型 | 指令示例 | 说明 |
52+
| -------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53+
| **LoadLoad** | `Load1; LoadLoad; Load2` | 保证 `Load1` 的读取操作在 `Load2` 及其后续读取操作之前完成 |
54+
| **StoreStore** | `Store1; StoreStore; Store2` | 保证 `Store1` 的写入操作对其他处理器可见(刷新到内存),先于 `Store2` 及其后续写入操作 |
55+
| **LoadStore** | `Load1; LoadStore; Store2` | 保证 `Load1` 的读取操作在 `Store2` 及其后续写入操作刷新到内存之前完成 |
56+
| **StoreLoad** | `Store1; StoreLoad; Load2` | 保证 `Store1` 的写入操作对其他处理器可见,先于 `Load2` 及其后续读取操作。`StoreLoad` 屏障的开销是四种屏障中最大的,它同时具有其他三种屏障的效果,因此也称为 **全能屏障(Full Barrier)** |
5757

5858
#### volatile 读写操作的内存屏障插入策略
5959

@@ -181,7 +181,7 @@ public class VolatileHappensBeforeDemo {
181181

182182
根据 **传递性**:操作1、操作2 happens-before 操作5、操作6。
183183

184-
因此,当线程 B 在操作4 读取到 `flag == true` 时,线程 A 在操作3 之前对 `a``b` 的修改对线程 B 一定是可见的。这里的关键在于:**volatile 变量的写-读操作,不仅保证了 volatile 变量本身的可见性,还通过 happens-before 的传递性"顺带"保证了其前后普通变量的可见性。**
184+
因此,当线程 B 在操作4 读取到 `flag == true` 时,线程 A 在操作3 之前对 `a``b` 的修改对线程 B 一定是可见的。这里的关键在于:**volatile 变量的写-读操作,不仅保证了 volatile 变量本身的可见性,还通过 happens-before 的传递性“顺带”保证了其前后普通变量的可见性。**
185185

186186
这也解释了为什么在实际开发中,`volatile` 经常被用作 **状态标志位**(如上面例子中的 `flag`),它可以在不使用锁的情况下,安全地在线程间传递状态信息,同时保证相关数据的可见性。
187187

@@ -338,7 +338,7 @@ sum.increment();
338338
1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-\$50 )。
339339
2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-\$20 )。
340340
3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=\$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。
341-
4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 的乐观锁策略,因此,操作员 B 的提交被驳回。
341+
4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=\$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 的乐观锁策略,因此,操作员 B 的提交被驳回。
342342

343343
这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。
344344

@@ -730,13 +730,13 @@ Open JDK 官方声明:[JEP 374: Deprecate and Disable Biased Locking](https://
730730
731731
二者性能差异的根本原因在于底层实现机制不同:
732732

733-
| 对比维度 | `volatile` | `synchronized` |
734-
| --- | --- | --- |
735-
| **实现层面** | 通过插入内存屏障指令实现,不涉及线程阻塞和上下文切换 | 依赖操作系统的互斥锁(Mutex Lock),涉及用户态与内核态的切换 |
736-
| **读操作开销** | 与普通变量几乎相同 | 需要获取 monitor 锁,即使无竞争也有一定开销(偏向锁/轻量级锁 CAS) |
737-
| **写操作开销** | 需要插入 `StoreStore` + `StoreLoad` 内存屏障,有一定开销但不会导致线程阻塞 | 需要获取和释放 monitor 锁,有竞争时会导致线程阻塞和上下文切换 |
738-
| **竞争时的表现** | 不会导致线程阻塞,始终是非阻塞的 | 线程竞争激烈时,会频繁发生阻塞和唤醒,上下文切换开销大 |
739-
| **功能范围** | 只能修饰变量,只保证可见性和有序性 | 可以修饰方法和代码块,同时保证可见性、有序性和原子性 |
733+
| 对比维度 | `volatile` | `synchronized` |
734+
| ---------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------ |
735+
| **实现层面** | 通过插入内存屏障指令实现,不涉及线程阻塞和上下文切换 | 依赖操作系统的互斥锁(Mutex Lock),涉及用户态与内核态的切换 |
736+
| **读操作开销** | 与普通变量几乎相同 | 需要获取 monitor 锁,即使无竞争也有一定开销(偏向锁/轻量级锁 CAS) |
737+
| **写操作开销** | 需要插入 `StoreStore` + `StoreLoad` 内存屏障,有一定开销但不会导致线程阻塞 | 需要获取和释放 monitor 锁,有竞争时会导致线程阻塞和上下文切换 |
738+
| **竞争时的表现** | 不会导致线程阻塞,始终是非阻塞的 | 线程竞争激烈时,会频繁发生阻塞和唤醒,上下文切换开销大 |
739+
| **功能范围** | 只能修饰变量,只保证可见性和有序性 | 可以修饰方法和代码块,同时保证可见性、有序性和原子性 |
740740

741741
**选择建议:**
742742

docs/java/concurrent/java-concurrent-questions-03.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,13 @@ Thread ──→ ThreadLocalMap ──→ Entry ─── key(WeakReference)
184184

185185
当业务代码中的 `ThreadLocal` 引用被置为 `null` 后,由于 Entry 的 key 是弱引用,`ThreadLocal` 实例在下次 GC 时会被回收,key 变为 `null`。此时虽然 value 仍然存在(强引用),但 `ThreadLocalMap` 在执行 `get()``set()``remove()` 等操作时,会主动探测并清理这些 key 为 `null` 的 "stale entry"(过期条目),从而释放 value 对象。
186186

187-
也就是说,**弱引用的设计是一种"兜底"防御机制**——即便开发者忘记调用 `remove()`,JVM 的 GC 配合 `ThreadLocalMap` 的自清理逻辑,仍然有机会回收泄漏的数据。而如果使用强引用,一旦忘记 `remove()`,就完全没有任何补救机会了。
187+
也就是说,**弱引用的设计是一种“兜底”防御机制**——即便开发者忘记调用 `remove()`,JVM 的 GC 配合 `ThreadLocalMap` 的自清理逻辑,仍然有机会回收泄漏的数据。而如果使用强引用,一旦忘记 `remove()`,就完全没有任何补救机会了。
188188

189189
> 需要注意的是,这种自清理机制是**被动触发**的(只在 `get`/`set`/`remove` 操作时顺便清理),并不能保证所有过期条目都被及时清理。因此,**弱引用只是降低了内存泄漏的风险,并没有彻底消除它**,手动调用 `remove()` 仍然是必须的。
190190
191191
#### 线程池场景下的特殊风险
192192

193-
上面提到内存泄漏的条件之一是"线程持续存活"。在使用 `new Thread()` 创建线程的场景下,线程执行完毕后会被销毁,其持有的 `ThreadLocalMap` 也会随之被 GC 回收,泄漏的影响相对有限。
193+
上面提到内存泄漏的条件之一是线程持续存活。在使用 `new Thread()` 创建线程的场景下,线程执行完毕后会被销毁,其持有的 `ThreadLocalMap` 也会随之被 GC 回收,泄漏的影响相对有限。
194194

195195
但在**线程池**场景下,问题会被严重放大。线程池中的核心线程默认不会被销毁,它们会被反复复用来执行不同的任务。这意味着:
196196

@@ -203,7 +203,7 @@ Thread ──→ ThreadLocalMap ──→ Entry ─── key(WeakReference)
203203

204204
#### 阿里巴巴 Java 开发手册的强制规约
205205

206-
正因为线程池 + `ThreadLocal` 的组合如此容易踩坑,《阿里巴巴 Java 开发手册》在"并发处理"章节中对此做出了**强制**级别的要求:
206+
正因为线程池 + `ThreadLocal` 的组合如此容易踩坑,《阿里巴巴 Java 开发手册》在并发处理章节中对此做出了**强制**级别的要求:
207207

208208
> **【强制】** 必须回收自定义的 `ThreadLocal` 变量记录的当前线程的值,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 `ThreadLocal` 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 `try-finally` 块进行回收。
209209
@@ -810,7 +810,7 @@ public class ThreadPoolTest {
810810

811811
![将一部分任务保存到MySQL中](https://oss.javaguide.cn/github/javaguide/java/concurrent/threadpool-reject-2-threadpool-reject-02.png)
812812

813-
整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免"饥饿"问题。
813+
整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免“饥饿”问题。
814814

815815
当然,对于这个问题,我们也可以参考其他主流框架的做法,以 Netty 为例,它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务,为了保证任务的实时处理,这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控:
816816

0 commit comments

Comments
 (0)