《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile(四)

简介: 《JUC并发编程 - 原理篇》Monitor | synchronized | wait&notify | join | park&unpark | 指令级并行 | volatile

4. 内存屏障

Memory Barrier(Memory Fence)

  • 可见性
  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
  • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
  • 有序性
  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

4c7f9aaf185b613544b93b1d63ebc69c.png


九、volatile 原理


volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)


对 volatile 变量的写指令后会加入写屏障,在写指令包括之前的写都会被同步到主存中去

对 volatile 变量的读指令前会加入读屏障,包括这条以及之后的读都是从主存中读


1.如何保证可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready 是 volatile 赋值带写屏障
    // 写屏障
}

而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

public void actor1(I_Result r) {
  // 读屏障
  // ready 是 volatile 读取值带读屏障
    if(ready) {
        r.r1 = num + num;
    } else {
        r.r1 = 1;
    }
}


80d202e39ce5d27275bd726fca24d31f.png

2.如何保证有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

public void actor2(I_Result r) {
    num = 2;
    ready = true; // ready 是 volatile 赋值带写屏障
    // 写屏障
}

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

public void actor1(I_Result r) {
    // 读屏障
    // ready 是 volatile 读取值带读屏障
    if(ready) {
      r.r1 = num + num;
    } else {
      r.r1 = 1;
    }
}


b61de92159b416a4529aa598317b1299.png

注意:volatile只是保证可见性和有序性,不能解决指令交错:


写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证其他线程的读跑到它前面去

如:t1写入i=1可以保证 结果会被刷新到主存,但是并不能保证t2读的顺序(如图t2)

而有序性的保证也只是保证了本线程内相关代码不被重排序

两(多)个线程之间指令的顺序是由CPU的时间片决定的


d4b068a97904c4797bbeb1dcc1de75ef.png

synchronized和volatile:前者可以保证原子性、可见性、有序性,后者只能保证可见性、有序性,但是无法保证原子性。

3.double-checked locking 问题

**引入:**double-checked的由来

35cddffdc4e373bdc3a233c20ab5e500.png

以著名的 double-checked locking 单例模式为例

public final class Singleton {
    private Singleton() { }
    private static Singleton INSTANCE = null;
    public static Singleton getInstance() {
        if(INSTANCE == null) { // t2
            // 首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class) {
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}


以上的实现特点是:


懒惰实例化

首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:


0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn
//注意:new 是在堆中创建了一个Singleton实例对象,dup是复制了指针引用放入栈中

其中 下面这几条指令正常情况下是会按照顺序依次执行的:


17 表示创建对象,将对象引用入栈 // new Singleton

20 表示复制一份对象引用 // 引用地址

21 表示利用一个对象引用,// 调用构造方法

24 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:


1e642c736da7b7b94237d20d6384e77e.png

**分析:**先调用24行在调用21行,也就是发生了指令重排。在t1执行24行时,t2正执行执行 0行和3行代码,这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例


关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取INSTANCE 变量的值


解决办法:对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效


补充:synchronized块内可以防止指令重排嘛?


synchronized不能防止指令重排序( 块里的非原子操作依旧可能发生指令重排序),但是**volatile可以防止指令重排序** 。


但是如果共享变量完全被synchronized所保护,那么在使用过程中是不会有原子、有序、可见性问题的。即使出现了重排序,也不会出现有序性问题的!我们的案例中instance并没有被完全保护起来,所以会导致下图中现象会发生,同时由于指令的重排序就导致了 上面分析中出现的 问题


dbc7ccfdc0ab78acc03a94cf23d92433.png


重排序和有序性不是一回事,synchronized的有序性是持有相同锁的两个同步块只能串行的进入,即被加锁的内容要按照顺序被多个线程执行,但是其内部的同步代码还是会发生重排序,使块与块之间有序可见。


参考文章:Java synchronized 能防止指令重排序吗?


4. double-checked locking 解决

public final class Singleton {
    private Singleton() { }
    private static volatile Singleton INSTANCE = null;//可以防止指令重排序
    public static Singleton getInstance() {
    // 实例没创建,才会进入内部的 synchronized代码块
        if (INSTANCE == null) {
            synchronized (Singleton.class) { // t2
        // 也许有其它线程已经创建实例,所以再判断一次
                if (INSTANCE == null) { // t1
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

字节码上看不出来 volatile 指令的效果

// -------------------------------------> 加入对 INSTANCE 变量的读屏障 防止屏障之后的代码排到前面去
0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
3: ifnonnull 37
6: ldc #3 // class cn/itcast/n5/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
14: ifnonnull 27
17: new #3 // class cn/itcast/n5/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障 防止屏障之前的代码排到后面去
27: aload_0
28: monitorexit ------------------------> 保证原子性、可见性
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
40: areturn

如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面两点:


可见性


写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中

而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据

有序性


写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

更底层是读写变量时使用 lock 指令来保证多核 CPU 之间的可见性与有序性


加入volatile后执行图解

b3d2f063529d36a16c5fb547ef8e659c.png


相关文章
|
缓存 监控 API
抖音抖店 API 请求获取宝贝详情数据的调用频率限制如何调整?
抖音抖店API请求获取宝贝详情数据的调用频率受限,需遵循平台规则。开发者可通过提升账号等级、申请更高配额、优化业务逻辑(如缓存数据、异步处理、批量请求)及监控调整等方式来应对。
|
4月前
|
缓存 安全 Java
说一说如何理解深浅拷贝、Immutable、保护性拷贝
我是小假 期待与你的下一次相遇 ~
|
缓存 NoSQL Java
避免缓存失效的三大杀手:缓存击穿、穿透与雪崩的解决方案
避免缓存失效的三大杀手:缓存击穿、穿透与雪崩的解决方案
1601 0
|
弹性计算 负载均衡 调度
docker swarm 使用详解
docker swarm 使用详解
341 0
|
存储 设计模式 前端开发
什么是SpringMVC?简单好理解!什么是应用分层?SpringMVC与应用分层的关系? 什么是三层架构?SpringMVC与三层架构的关系?
文章解释了SpringMVC的概念和各部分功能,探讨了应用分层的原因和具体实施的三层架构,以及SpringMVC与三层架构之间的关系和联系。
698 1
什么是SpringMVC?简单好理解!什么是应用分层?SpringMVC与应用分层的关系? 什么是三层架构?SpringMVC与三层架构的关系?
|
开发工具 git
git如何修改提交代码时的名字和邮箱?
git如何修改提交代码时的名字和邮箱?
3843 4
|
SQL 数据可视化 关系型数据库
2022年最新最详细IDEA关联数据库方式、在IDEA中进行数据库的可视化操作(包含图解过程)
这篇文章详细介绍了如何在IntelliJ IDEA中关联MySQL数据库,包括打开Database侧边栏、选择数据库、输入连接信息、测试连接,并提供了解决连接问题的方案,以及在IDEA中进行数据库的可视化操作步骤。
2022年最新最详细IDEA关联数据库方式、在IDEA中进行数据库的可视化操作(包含图解过程)
|
人工智能 Java 数据库
Google Earth Engine(GEE)——北美当前和预测的气候数据
Google Earth Engine(GEE)——北美当前和预测的气候数据
321 0
|
缓存 安全 Java
Nacos常见问题之注册不上如何解决
Nacos是一款易于使用的动态服务发现、配置管理和服务管理平台,针对不同版本可能出现的兼容性和功能问题,本汇总贴心整理了用户在使用Nacos时可能遇到的版本相关问题及答案,以便用户能够更顺畅地进行服务治理和配置管理。
1786 0
|
Java Android开发
Android8.1 出厂前默认开启USB调试且自动授权,恢复出厂关闭USB调试方案
Android8.1 出厂前默认开启USB调试且自动授权,恢复出厂关闭USB调试方案
1065 0