为什么 synchronized 不能防止指令重排序?

在某乎看到一个提问,大家讨论 synchronized 能不能防止指令冲排序,咋说的都有,我发现大家学习底层技术很多时候会有误区。

先说我的观点:synchronized 绝对不能防止它内部代码的指令重排序!

下边说说我的分析哈,不对的大家讨论

synchronized 到底防了什么?

很多人觉得 synchronized 能防重排序,是因为看到了它能保证有序性这句话。但这句话是有大前提的!

打个比方:synchronized 就像是一间带锁的单人洗手间。

并发视角的有序性,线程 A 进去了,把门反锁;线程 B 只能在门外排队。A 出来之后,B 才能进去。线程 B 看来,A 在里面的所有动作是一次性做完的,这就叫保证了有序性和原子性。

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

只要 A 在洗手间里的瞎搞不影响最终结果,CPU 就觉得没毛病。synchronized 根本管不住 CPU 在单线程内部的微操。

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

我们来看看经典的 DCL 单例是怎么写的:

csharp 复制代码
public class Singleton {
    // 注意:这里如果不加 volatile,将引发灾难
    privatestatic Singleton instance; 

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

这段代码看着没啥毛病,外层判断空,避开锁的性能开销。内层加锁,保证只有一个线程去 new 对象。坑就坑在 instance = new Singleton() 这句代码上。

因为 Java 字节码CPU 执行层面,他就不是一个原子操作,它被分成三步:

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 杀过来了。线程 B 执行第一句代码 if (instance == null)。注意这句代码是在 synchronized 外面的!
  • 线程 B 根本不需要等锁!线程 B 看到 instance != null,对象 new 好了,直接 return instance 拿去用。
  • 结果线程 B 拿到的是一个还没执行构造函数的半成品对象,线程 B 调用里面的成员变量,直接爆出 NullPointerException 或者拿到错乱的初始值,系统当场崩溃。

发现问题了吗?

synchronized 确实能把线程 A 锁在里面,但它防不住线程 B 在外面偷看!因为 DCL 最大的卖点就是第一层检查没有加锁!

这就是为什么必须给 instance 加上 volatile 关键字

arduino 复制代码
private static volatile Singleton instance;

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

加了 volatile 之后,CPU 和编译器看到这个变量,就会老老实实地立正站好。它强制要求必须先完全执行完步骤 1 和 2 ,才能执行步骤 3。

这样一来,只要线程 B 看到 instance != null,那么这个对象绝对是可用的,不会拿到半成品。

相关推荐
运行时异常1 小时前
【WMS 仓储系统集成 AI Agent 实战】第 3 讲:Spring Security 6 + JWT——addFilterBefore 一词之差,全站 401
java·后端
LinMINGJing0071 小时前
PageHeaderData:Page 的页面头
后端
2601_962069771 小时前
SpringBoot开发——初步了解SpringBoot
java·spring boot·后端
jay神1 小时前
【计算机毕业设计】基于Spring Boot的宠物领养管理系统
java·spring boot·后端·vue·毕业设计·宠物
2601_962056602 小时前
可造成敏感信息泄露!Spring Boot之Actuator信息泄露漏洞三种利用方式总结
java·spring boot·后端
码视野2 小时前
基于 Spring Boot + Vue3 的【城市道路地下空洞塌陷微动传感监测与路面脱空沉降预警中台】设计与实现(含PRD/三端高保真源码/大屏)
vue.js·spring boot·后端
2601_962181964 小时前
Spring boot从0到1 - day01
java·spring boot·后端
码视野4 小时前
基于 Spring Boot + Vue3 的【智慧公厕微负压排风除臭与客流人感空间占用导引中台】设计与实现(含PRD/三端高保真源码/大屏)
前端·vue.js·人工智能·spring boot·后端
ERD Online4 小时前
Cursor 连上 MCP:读一张 ER 图,提交一版建议
数据库·后端·开源·cursor·mcp
思考着亮5 小时前
4.依赖注入_Depends用法_四大应用场景_Yield生成器原理.
后端