找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2517

积分

0

好友

337

主题
发表于 3 小时前 | 查看: 3| 回复: 0

在某乎上看到一个关于 synchronized 能否防止指令重排序的讨论,发现观点不一,这反映出很多开发者在学习底层技术时存在一个误区。

我的核心观点很明确:synchronized 绝对不能防止其同步块内部代码的指令重排序!

下面我们来具体分析一下。

synchronized 到底保证了什么?

很多人认为 synchronized 能防止重排序,是因为经常看到“synchronized 能保证有序性”这句话。然而,这句话有一个重要的大前提!

我们可以打个比方:synchronized 就像一间带锁的单人洗手间。

从并发视角来看它的有序性保证:线程 A 进去后把门反锁,线程 B 只能在门外排队。A 出来之后,B 才能进去。对于线程 B 来说,线程 A 在洗手间里的所有动作是“一次性”完成的。这正是 synchronized 保证的 线程间执行顺序的有序性 和原子性。

但是,问题出在里面。线程 A 关上门之后,在洗手间里到底是先脱裤子再上厕所,还是先洗脸再刷牙?为了追求极致的执行效率,CPU 和编译器完全可能把线程 A 在洗手间里的这些动作顺序打乱 —— 这就是 指令重排序

只要线程 A 在里面的一系列“微操”不影响单线程执行下的最终结果,CPU 就觉得没毛病。而 synchronized 这个“门锁”,根本管不住 CPU 在单线程内部进行的这种优化。

双重检查锁DCL为什么要加volatile?

我们来看一个经典的双重检查锁(Double-Checked Locking, DCL)单例的实现,问题就出在这里:

public class Singleton {
    // 注意:这里如果不加 volatile,将引发问题
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) { // <--- 第一重检查(在锁外面)
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(); // <--- 问题的根源
                }
            }
        }
        return instance;
    }
}

这段代码的逻辑看似完美:外层判空避免了不必要的加锁开销,内层加锁保证了只有一个线程能创建对象。但问题恰恰隐藏在 instance = new Singleton() 这一行。

Java 字节码和 CPU 执行的层面,new 操作并非原子操作,它大致分为三个步骤:

  1. 为对象分配内存空间
  2. 初始化对象(调用构造函数)
  3. instance 引用指向分配好的内存地址

正常人的逻辑顺序是 1 -> 2 -> 3。
然而,正如前面所说,synchronized 管不住“洗手间里面”的重排序。CPU 一看,步骤 2(初始化)和步骤 3(设置引用)之间没有数据依赖关系,为了提高效率,它可能会将顺序重排为 1 -> 3 -> 2。

灾难是如何发生的?

假设发生了上述的 1 -> 3 -> 2 重排序,我们来看看并发场景下会发生什么:

  • 线程 A 进入 synchronized 块,开始执行 new Singleton()
  • 线程 A 执行了步骤1(分配内存),紧接着执行了步骤3(设置引用)。此时,对象尚未初始化,但 instance 已经不再是 null 了!
  • 在这个极其危险的时刻,线程 B 恰好执行到第一重检查 if (instance == null)关键点在于,这行代码是在 synchronized 块之外的!
  • 线程 B 根本无需等待锁,它看到 instance != null,便以为对象已经创建完毕,直接 return instance 拿去使用。
  • 结果,线程 B 拿到的是一个尚未执行构造函数的“半成品”对象。一旦访问其成员变量,就可能引发 NullPointerException 或得到错误的初始值,导致系统出现难以排查的诡异问题。

你发现问题所在了吗?
synchronized 确实能把线程 A “锁”在同步块内完成操作,但它防不住线程 B 在同步块外进行“偷看”!因为 DCL 模式设计的初衷,就是让第一重检查绕开锁以提升性能。

volatile 的关键作用

这正是为什么在 DCL 单例中,必须给 instance 变量加上 volatile 关键字:

private static volatile Singleton instance;

volatile 关键字除了保证变量的内存可见性之外,在这个场景下最核心的作用是插入内存屏障,禁止指令重排序

当变量被 volatile 修饰后,CPU 和编译器在处理与之相关的操作时就会“立正站好”。它会强制要求 new 操作的三个步骤必须按照 1 -> 2 -> 3 的顺序执行,禁止步骤 2 和 3 的重排序。

这样一来,只要任何线程看到 instance != null,那么这个 Singleton 对象就一定是一个完全初始化好的、可用的对象,彻底杜绝了拿到“半成品”的风险。

总结

通过上面的分析,我们可以清晰地认识到:
Synchronized 保证的是多个线程之间访问临界区的有序性(一个接一个执行),它通过互斥锁来实现这一点。但它并不能约束单一线程内部,由编译器或 CPU 为了优化性能而进行的指令重排序

在诸如 DCL 单例这类对对象初始化完成状态有严格要求的精密并发场景中,必须借助 volatile禁止重排序语义来与 synchronized 配合,才能构建出线程安全的代码。理解这些 多线程 与内存模型的底层细节,对于编写正确、高效的高并发程序至关重要。如果你想深入探讨更多类似的问题,欢迎到技术社区进行交流。




上一篇:2026年2月企业必修安全漏洞清单:涵盖OpenClaw、Gradio等9项高危风险
下一篇:Linux系统管理chown命令详解:从基础语法到递归操作与符号链接处理
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-18 12:05 , Processed in 0.465626 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表