无标题
IO操作会发生系统调用,而系统调用会发生用户态和内核态切换,缓存会失效,寄存器就更不用说了,系统调用返回的时候会重新从主存载入数据(有可能,但不可靠、不可预测、不可依赖)。
1. JSR-133 (Java Memory Model) FAQ
其中明确指出:
“The memory model describes what behaviors are legal in multithreaded code. It does not try to specify how those behaviors are implemented — that’s up to the JVM and the underlying hardware.”
“内存模型描述了多线程代码中的合法行为。它并不试图规定这些行为如何实现——这由 JVM 和底层硬件决定。”
2. Java Language Specification (JLS) §17.4
定义了 happens-before、synchronization order 等抽象规则,但
✅ 总结
| 问题 | 回答 |
|---|---|
| “主存”和“工作内存”是 Java 还是操作系统的概念? | ✅ 是 |
| 它们和操作系统内存有联系吗? | ✅ ✅ |
| JMM 如何保证可见性? | 通过 |
| 能否绕过 JMM 直接依赖操作系统? | ❌ 不推荐。行为不可移植、不可预测。应使用 JMM 提供的同步机制 |
我们分情况看:
| 情况 | 是否可能重新加载 |
|---|---|
| stop | ✅ 有可能 |
| stop | ✅ 有可能 |
| stop | ❌ 不会,即使缓存刷新也读不到 |
| 没有 | ❌ 即使缓存刷新,也不会重新读内存 |


🔍 为什么加 System.out.println() 就能跳出循环?
✅ 正确答案:因为 System.out.println() 是一个同步方法(synchronized),它会触发内存屏障(Memory Barrier),使得 stop 变量的修改对工作线程可见。
详细解释:
1. 内存可见性问题(Visibility)
主线程修改了 stop = true。
但是工作线程可能一直在自己的 CPU 缓存 或 寄存器 中读取 stop 的值。
如果没有正确的同步机制,工作线程 永远看不到 stop 被改为 true,于是陷入无限循环。
2. System.out.println()** 做了什么?**
System.out 是一个 PrintStream 对象。
println() 方法是 synchronized 的:
1 | java深色版本public void println() { |
synchronized 会:
在进入时(monitorenter)插入 Load Barrier,强制从主内存重新加载变量。
在退出时(monitorexit)插入 Store Barrier,强制将修改写回主内存。
所以,System.out.println() 虽然看似只是打印,但它是一个 同步块,会强制线程与主内存同步,从而看到 stop 的最新值。
❌ 为什么去掉 System.out.println() 会卡住?
没有同步操作,JIT 编译器可能会 优化掉对 stop 的重复读取,认为它不会改变(即使它不是 volatile)。
工作线程可能将 stop 缓存在寄存器中,永远读到 false,导致无限循环。
✅ 正确解决方式(不要依赖 println())
你应该使用正确的并发机制来保证可见性:
✅ 方法1:使用 volatile 关键字
1 | java深色版本private static volatile boolean stop = false; |
volatile 保证变量的修改对所有线程 立即可见。
禁止指令重排序。
是解决此类问题的标准做法。
✅ 方法2:使用 synchronized
1 | java深色版本while (true) { |
✅ 方法3:使用 AtomicBoolean
1 | java深色版本private static AtomicBoolean stop = new AtomicBoolean(false); |
🔬 实验验证
你可以尝试以下对比:
| 情况 | 是否跳出循环 | 原因 |
|---|---|---|
| 有 | ✅ 会跳出 | 同步方法触发内存同步 |
| 没有 | ❌ 可能卡住 | 缓存 + JIT 优化导致看不到修改 |
| 加 | ✅ 会跳出 | 正确的可见性保证 |
| 加 | ✅ 会跳出 | sleep 是 native 方法,也触发同步 |
🚫 注意:不要依赖 System.out.println() 来“修复”并发问题!
虽然它“碰巧”有效,但这是
🔍 为什么 FileOutputStream.write() 能跳出循环?(根本原因)
虽然
🧱 原因 1:write() 是 native 方法 → 触发 Safe Point 检查
这是最关键的原因!
在 HotSpot JVM 中:
所有 native 方法调用 都是潜在的 Safe Point(安全点)。
当线程从 Java 代码进入 native 代码(或返回)时,JVM 有机会检查是否需要:
响应线程中断
执行垃圾回收
重新加载共享变量的最新值(从主内存)
👉
这意味着:即使
🔗 参考 HotSpot 源码:src/hotspot/share/runtime/safepoint.hpp 和 sharedRuntime.cpp
🧱 原因 2:JIT 编译器不敢过度优化包含 native 调用的循环
JIT 编译器(C2)在优化循环时,会对“纯 Java 代码”做激进优化,比如:
1 | java深色版本// JIT 可能优化为: |
但一旦循环中包含
“这个方法可能产生副作用(side effect)—— 它可能修改了内存、文件、网络等。”
因此,JVM
这就相当于
🧱 原因 3:系统调用本身可能间接影响内存一致性
虽然
write() 系统调用会触发用户态 → 内核态切换
内核执行期间,CPU 缓存状态可能被刷新(如 TLB invalidation)
多核 CPU 通过 MESI 缓存一致性协议 检测到内存修改
主线程修改 stop = true 后,该变量所在的缓存行可能被标记为“已修改”
当工作线程下一次访问该内存区域时(即使不是直接读 stop),也可能被动更新
虽然这不是“保证”,但在现代 x86 架构上,
✅ 对比:System.out.println() vs FileOutputStream.write()
| 特性 | System.out.println() | FileOutputStream.write() |
|---|---|---|
| 是否 | ✅ 是 | ❌ 否 |
| 是否 native 调用 | ✅ 是 ( | ✅ 是 |
| 是否触发 Safe Point | ✅ 是 | ✅ 是 |
| 是否有副作用(I/O) | ✅ 是 | ✅ 是 |
| JIT 是否敢优化掉 | ❌ 不敢(双重保险) | ❌ 不敢(至少不敢太激进) |
| 跳出循环的根本原因 | synchronized | Safe Point + JIT 保守优化 |
👉 所以:
**native 方法调用 → 触发 Safe Point + 阻止 JIT 过度优化 → 被迫重新读取内存 → 看到 **stop = true
📌 官方态度:这仍然是“未定义行为”
尽管它“能工作”,但
📚 来自 JSR-133 FAQ 的原话:
“Just because a thread performs an I/O operation does not mean that it will see the latest value of a non-volatile variable written by another thread.”
“仅仅因为一个线程执行了 I/O 操作,并不意味着它能看到另一个线程对非 volatile 变量的最新写入。”
🔗
✅ 实验验证:什么时候它会“失效”?
你可以尝试以下情况,
实验 1:关闭 JIT 优化
1 | bash深色版本java -Xint YourClass # 解释执行,无 JIT |
→ 可能仍然跳出(Safe Point 仍在)
实验 2:使用 ByteBuffer + FileChannel 写入(减少系统调用频率)
1 | java深色版本FileChannel channel = fos.getChannel(); |
→ 如果
实验 3:在嵌入式或实时系统上运行
→ 缓存一致性、Safe Point 行为可能不同,行为不可预测。
✅ 结论
FileOutputStream.write()** 能跳出循环,是因为:**它是 native 方法,触发 Safe Point,迫使 JVM 重新检查内存状态;它有副作用,阻止 JIT 编译器将 stop 缓存在寄存器中;系统调用和缓存一致性协议可能间接帮助传播变量更新。但它不是由 Java 内存模型(JMM)保证的,属于“实现相关”的副作用,不可依赖。
✅ 正确做法(再次强调)
1 | java深色版本private static volatile boolean stop = false; // ✅ 正确 |
或:
1 | java深色版本private static final AtomicBoolean stop = new AtomicBoolean(false); |
这才是
💡 记住:“它能运行” ≠ “它是正确的”“它在你的机器上有效” ≠ “它在所有 JVM 上都有效”
不要用 I/O 当“魔法同步工具” ✨🚫
IO模型
当调用一次channel.read,stream.read后,会切换置操作系统内核态来完成真正的数据读取,而读取又分为两个阶段,等待数据阶段和复制数据阶段

阻塞IO同步()
当用户空间发起一次read,就会切换到操作系统的内核空间,但是网络可能没有数据发送过来,所以线程停下来等待数据,不光是用户线程 等,内核空间也会等待,当有数据时,就会从网卡复制数据到内存当中去,复制完后,就会从内核空间切换为用户空间,这种模式下用户现成就被阻塞住,在读取期间什么事情都干不了
非阻塞IO(同步)
非阻塞模式下,一般是在while true循环,在等待数据阶段,调一次read方法,如果没有数据,会立刻返回结果,有数据后才会进入复制数据的阶段, 而在复制数据阶段,用户线程实际还是阻塞住的,等待阶段完成,用户线程再继续往下运行,所以这个模型是在等待数据阶段非阻塞的,并且在等待数据阶段他会多次进行用户空间和内核空间的切换,可能会影响性能。
多路复用(同步)
阻塞io的问题是,比如在一个单线程中的循环中,不光要处理read还要处理accept,channel1处理完后,在等待channel2建立连接时,阻塞IO下就一直等待着,期间就处理不了channel1发送的数据,必须得等到channel2建立完之后才能去处理channel1

多路复用:一个selector就能去监测多个channel的事件,一次性的把多个channel上的事件进行处理

同步
线程自己去获取结果(一个线程)
异步
线程自己不去获取结果,而是由其他线程送结果(至少两个线程),没有异步阻塞的说法,如果异步还阻塞住了,为什么不同步做这件事?
零拷贝
传统IO问题
传统的IO将一个文件通过socket写出:

流程:

1,java本身并不具备IO读写能力,因此read方法调用后,要从java程序的用户态切换至内核态,去调用操作系统的读能力,将数据读入内核缓冲区,这期间用户线程阻塞,操作系统使用DMA来实现文件读,期间不会使用cpu,DMA可以理解为硬件单元,用来释放cpu完成文件IO
2,从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即byte[] buf),这期间cpu会参与拷贝,无法利用DMA
3,调用wirte方法,这时将数据从用户缓冲区即(byte[] buf)写入socket缓冲区,cpu会参与拷贝
4,接下来要向网卡写数据,这项能力java又不具备,因此又得从用户态切换至内核态,代用操作系统的写能力,使用DMA像socket缓冲区的数据写入网卡,不会使用cpu
用户态和内核态切换发生了3次,这个操作比较重量级
数据拷贝了共4次
NIO优化
通过DirectByteBuf
ByteBUffer.allocate(10) HeapByteBuffer使用的还是java内存
ByteBUffer.allocateDirect(10) DirectByteBuffer使用的是操作系统内存

java可以使用DIrectByteBuf将堆外内存映射到jvm内存中来直接访问:
这块内存不受jvm垃圾回收的影响,因此内存地址固定,有助于IO读写
java中的DirectByteBuf对象仅维护了此内存的虚引用,内存回收分成两步,1 DirectByteBuf对象被垃圾回收,将虚引用加入引用队列,2 通过专门线程访问引用队列,根据虚引用释放堆外内存
减少了一次数据拷贝,用户态和内核态的切换次数没有减少
进一步优化(底层采用了linux2.1后提供的sendFile方法),java中对应两个channel调用transferTo/transferFrom方法拷贝数据

1,java调用transferTo方法后,要从java程序的用户态切换至内核态,使用DMA将数据读入内核缓冲区,不会使用cpu
2,数据从内核缓冲区传输到socket缓冲区,cpu会参与拷贝
3,最后使用DMA将socket缓冲区的数据写入网卡,不会使用cpu
可以看到:
只发生了一次用户态和内核态的切换
数据拷贝了3次
进一步优化(linux2.4)

1,java使用transferTo方法后,要从java程序的用户态切换至内核态,使用DMA级昂数据读入内核缓冲区,不会使用cpu
2,只会将一些offset和length信息考入到socket缓冲区,几乎无消耗
3,使用DMA将内核缓冲区的数据写入网卡,不会使用cpu
只发生一次用户态和内核态的切换,数据拷贝了2次,所谓的零拷贝,并不是真正无拷贝,而是不会拷贝重复数据到jvm内存中,零拷贝的优点:
更少的用户态和内核态的切换
不利用cpu的计算,减少cpu缓存伪共享
零拷贝适合小文件传输(缓存区的东西如果大了就无意义)
