为什么 synchronized 不能防止指令重排序?
在某乎看到一个提问大家讨论synchronized能不能防止指令冲排序咋说的都有我发现大家学习底层技术很多时候会有误区。先说我的观点synchronized 绝对不能防止它内部代码的指令重排序下边说说我的分析哈不对的大家讨论synchronized 到底防了什么很多人觉得 synchronized 能防重排序是因为看到了它能保证有序性这句话。但这句话是有大前提的打个比方synchronized 就像是一间带锁的单人洗手间。并发视角的有序性线程 A 进去了把门反锁线程 B 只能在门外排队。A 出来之后B 才能进去。线程 B 看来A 在里面的所有动作是一次性做完的这就叫保证了有序性和原子性。但是线程 A 关上门之后在洗手间里到底是先脱裤子再上厕所还是先脱衣服再洗脸CPU 和编译器为了追求极致的执行效率是完全可能把 A 在洗手间里的动作顺序打乱的指令重排序。只要 A 在洗手间里的瞎搞不影响最终结果CPU 就觉得没毛病。synchronized 根本管不住 CPU 在单线程内部的微操。双重检查锁DCL为什么要加volatile我们来看看经典的 DCL 单例是怎么写的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 关键字private static volatile Singleton instance;volatile 的核心作用除了大家熟知的保证内存可见性之外在 DCL 这个场景下最关键的作用是插入内存屏障禁止指令重排序。加了 volatile 之后CPU 和编译器看到这个变量就会老老实实地立正站好。它强制要求必须先完全执行完步骤 1 和 2 才能执行步骤 3。这样一来只要线程 B 看到 instance ! null那么这个对象绝对是可用的不会拿到半成品。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2422464.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!