Skip to content

Commit 1b78360

Browse files
committed
update:unsafe和线程池部分内容修正完善
1 parent 14ed3bd commit 1b78360

File tree

3 files changed

+68
-35
lines changed

3 files changed

+68
-35
lines changed

docs/cs-basics/operating-system/operating-system-basic-questions-02.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ LRU 算法是实际使用中应用的比较多,也被认为是最接近 OPT
379379

380380
### 提高文件系统性能的方式有哪些?
381381

382-
- **优化硬件**:使用高速硬件设备(如 SSD、NVMe)替代传统的机械硬盘,使用 RAID(Redundant Array of Inexpensive Disks)等技术提高磁盘性能。
382+
- **优化硬件**:使用高速硬件设备(如 SSD、NVMe)替代传统的机械硬盘,使用 RAID(Redundant Array of Independent Disks)等技术提高磁盘性能。
383383
- **选择合适的文件系统选型**:不同的文件系统具有不同的特性,对于不同的应用场景选择合适的文件系统可以提高系统性能。
384384
- **运用缓存**:访问磁盘的效率比较低,可以运用缓存来减少磁盘的访问次数。不过,需要注意缓存命中率,缓存命中率过低的话,效果太差。
385385
- **避免磁盘过度使用**:注意磁盘的使用率,避免将磁盘用满,尽量留一些剩余空间,以免对文件系统的性能产生负面影响。

docs/java/basis/unsafe.md

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -134,56 +134,91 @@ public native void freeMemory(long address);
134134
```java
135135
private void memoryTest() {
136136
int size = 4;
137-
long addr = unsafe.allocateMemory(size);
138-
long addr3 = unsafe.reallocateMemory(addr, size * 2);
139-
System.out.println("addr: "+addr);
140-
System.out.println("addr3: "+addr3);
137+
// 1. 分配初始内存
138+
long oldAddr = unsafe.allocateMemory(size);
139+
System.out.println("Initial address: " + oldAddr);
140+
141+
// 2. 向初始内存写入数据
142+
unsafe.putInt(oldAddr, 16843009); // 写入 0x01010101
143+
System.out.println("Value at oldAddr: " + unsafe.getInt(oldAddr));
144+
145+
// 3. 重新分配内存
146+
long newAddr = unsafe.reallocateMemory(oldAddr, size * 2);
147+
System.out.println("New address: " + newAddr);
148+
149+
// 4. reallocateMemory 已经将数据从 oldAddr 拷贝到 newAddr
150+
// 所以 newAddr 的前4个字节应该和 oldAddr 的内容一样
151+
System.out.println("Value at newAddr (first 4 bytes): " + unsafe.getInt(newAddr));
152+
153+
// 关键:之后所有操作都应该基于 newAddr,oldAddr 已失效!
141154
try {
142-
unsafe.setMemory(null,addr ,size,(byte)1);
143-
for (int i = 0; i < 2; i++) {
144-
unsafe.copyMemory(null,addr,null,addr3+size*i,4);
145-
}
146-
System.out.println(unsafe.getInt(addr));
147-
System.out.println(unsafe.getLong(addr3));
148-
}finally {
149-
unsafe.freeMemory(addr);
150-
unsafe.freeMemory(addr3);
155+
// 5. 在新内存块的后半部分写入新数据
156+
unsafe.putInt(newAddr + size, 33686018); // 写入 0x02020202
157+
158+
// 6. 读取整个8字节的long值
159+
System.out.println("Value at newAddr (full 8 bytes): " + unsafe.getLong(newAddr));
160+
161+
} finally {
162+
// 7. 只释放最后有效的内存地址
163+
unsafe.freeMemory(newAddr);
164+
// 如果尝试 freeMemory(oldAddr),将会导致 double free 错误!
151165
}
152166
}
153167
```
154168

155169
先看结果输出:
156170

157171
```plain
158-
addr: 2433733895744
159-
addr3: 2433733894944
160-
16843009
161-
72340172838076673
172+
Initial address: 140467048086752
173+
Value at oldAddr: 16843009
174+
New address: 140467048086752
175+
Value at newAddr (first 4 bytes): 16843009
176+
Value at newAddr (full 8 bytes): 144680345659310337
162177
```
163178

164-
分析一下运行结果,首先使用`allocateMemory`方法申请 4 字节长度的内存空间,调用`setMemory`方法向每个字节写入内容为`byte`类型的 1,当使用 Unsafe 调用`getInt`方法时,因为一个`int`型变量占 4 个字节,会一次性读取 4 个字节,组成一个`int`的值,对应的十进制结果为 16843009。
179+
`reallocateMemory` 的行为类似于 C 语言中的 realloc 函数,它会尝试在不移动数据的情况下扩展或收缩内存块。其行为主要有两种情况:
165180

166-
你可以通过下图理解这个过程:
181+
1. **原地扩容**:如果当前内存块后面有足够的连续空闲空间,`reallocateMemory` 会直接在原地址上扩展内存,并返回原始地址。
182+
2. **异地扩容**:如果当前内存块后面空间不足,它会寻找一个新的、足够大的内存区域,将旧数据拷贝过去,然后释放旧的内存地址,并返回新地址。
167183

168-
![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144344005.png)
184+
**结合本次的运行结果,我们可以进行如下分析:**
169185

170-
在代码中调用`reallocateMemory`方法重新分配了一块 8 字节长度的内存空间,通过比较`addr``addr3`可以看到和之前申请的内存地址是不同的。在代码中的第二个 for 循环里,调用`copyMemory`方法进行了两次内存的拷贝,每次拷贝内存地址`addr`开始的 4 个字节,分别拷贝到以`addr3``addr3+4`开始的内存空间上:
186+
**第一步:初始分配与写入**
171187

172-
![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144354582.png)
188+
- `unsafe.allocateMemory(size)` 分配了 4 字节的堆外内存,地址为 `140467048086752`
189+
- `unsafe.putInt(oldAddr, 16843009)` 向该地址写入了 int 值 `16843009`,其十六进制表示为 `0x01010101``getInt` 读取正确,证明写入成功。
173190

174-
拷贝完成后,使用`getLong`方法一次性读取 8 个字节,得到`long`类型的值为 72340172838076673。
191+
**第二步:原地内存扩容**
175192

176-
需要注意,通过这种方式分配的内存属于 堆外内存 ,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用`freeMemory`方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在`try`中执行对内存的操作,最终在`finally`块中进行内存的释放。
193+
- `long newAddr = unsafe.reallocateMemory(oldAddr, size * 2)` 尝试将内存块扩容至 8 字节。
194+
- 观察输出 New address: `140467048086752`,我们发现 `newAddr``oldAddr` 的值**完全相同**
195+
- 这表明本次操作触发了“原地扩容”。系统在原地址 `140467048086752` 后面找到了足够的空间,直接将内存块扩展到了 8 字节。在这个过程中,旧的地址 `oldAddr` 依然有效,并且就是 `newAddr`,数据也并未发生移动。
177196

178-
**为什么要使用堆外内存?**
197+
**第三步:验证数据与写入新数据**
179198

180-
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
181-
- 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
199+
- `unsafe.getInt(newAddr)` 再次读取前 4 个字节,结果仍是 `16843009`,验证了原数据完好无损。
200+
- `unsafe.putInt(newAddr + size, 33686018)` 在扩容出的后 4 个字节(偏移量为 4)写入了新的 int 值 `33686018`(十六进制为 `0x02020202`)。
201+
202+
**第四步:读取完整数据**
203+
204+
- `unsafe.getLong(newAddr)` 从起始地址读取一个 long 值(8 字节)。此时内存中的 8 字节内容为 `0x01010101` (低地址) 和 `0x02020202` (高地址) 的拼接。
205+
- 在小端字节序(Little-Endian)的机器上,这 8 字节在内存中会被解释为十六进制数 `0x0202020201010101`
206+
- 这个十六进制数转换为十进制,结果正是 `144680345659310337`。这完美地解释了最终的输出结果。
207+
208+
**第五步:安全的内存释放**
209+
210+
- `finally` 块中,`unsafe.freeMemory(newAddr)` 安全地释放了整个 8 字节的内存块。
211+
- 由于本次是原地扩容(`oldAddr == newAddr`),所以即使错误地多写一句 `freeMemory(oldAddr)` 也会导致二次释放的严重错误。
182212

183213
#### 典型应用
184214

185215
`DirectByteBuffer` 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。`DirectByteBuffer` 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。
186216

217+
**为什么要使用堆外内存?**
218+
219+
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
220+
- 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
221+
187222
下图为 `DirectByteBuffer` 构造函数,创建 `DirectByteBuffer` 的时候,通过 `Unsafe.allocateMemory` 分配内存、`Unsafe.setMemory` 进行内存初始化,而后构建 `Cleaner` 对象用于跟踪 `DirectByteBuffer` 对象的垃圾回收,以实现当 `DirectByteBuffer` 被垃圾回收时,分配的堆外内存一起被释放。
188223

189224
```java

docs/java/concurrent/java-thread-pool-summary.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@ tag:
1313

1414
## 线程池介绍
1515

16-
顾名思义,线程池就是管理一系列线程的资源池,其提供了一种限制和管理线程资源的方式。每个线程池还维护一些基本统计信息,例如已完成任务的数量。
17-
18-
这里借用《Java 并发编程的艺术》书中的部分内容来总结一下使用线程池的好处:
16+
池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。
1917

20-
- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
21-
- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。
22-
- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
18+
线程池提供了一种限制和管理资源(包括执行一个任务)的方式。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。使用线程池主要带来以下几个好处:
2319

24-
**线程池一般用于执行多个不相关联的耗时任务,没有多线程的情况下,任务顺序执行,使用了线程池的话可让多个不相关联的任务同时执行。**
20+
1. **降低资源消耗**:线程池里的线程是可以重复利用的。一旦线程完成了某个任务,它不会立即销毁,而是回到池子里等待下一个任务。这就避免了频繁创建和销毁线程带来的开销。
21+
2. **提高响应速度**:因为线程池里通常会维护一定数量的核心线程(或者说“常驻工人”),任务来了之后,可以直接交给这些已经存在的、空闲的线程去执行,省去了创建线程的时间,任务能够更快地得到处理。
22+
3. **提高线程的可管理性**:线程池允许我们统一管理池中的线程。我们可以配置线程池的大小(核心线程数、最大线程数)、任务队列的类型和大小、拒绝策略等。这样就能控制并发线程的总量,防止资源耗尽,保证系统的稳定性。同时,线程池通常也提供了监控接口,方便我们了解线程池的运行状态(比如有多少活跃线程、多少任务在排队等),便于调优。
2523

2624
## Executor 框架介绍
2725

0 commit comments

Comments
 (0)