剑指JUC原理-8.Java内存模型(中)

简介: 剑指JUC原理-8.Java内存模型

剑指JUC原理-8.Java内存模型(上):https://developer.aliyun.com/article/1413626


CPU 执行时间


程序的 CPU 执行时间,即我们前面提到的 user + system 时间,可以用下面的公式来表示


程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time


鱼罐头的故事


加工一条鱼需要 50 分钟,只能一条鱼、一条鱼顺序加工…

可以将每个鱼罐头的加工流程细分为 5 个步骤:


  • 去鳞清洗 10分钟
  • 蒸煮沥水 10分钟
  • 加注汤料 10分钟
  • 杀菌出锅 10分钟
  • 真空封罐 10分钟

即使只有一个工人,最理想的情况是:他能够在 10 分钟内同时做好这 5 件事,因为对第一条鱼的真空装罐,不会影响对第二条鱼的杀菌出锅…


指令重排序优化


事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令


还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在 80’s 中叶到 90’s 中叶占据了计算架构的重要地位。(分阶段,分工是提升效率的关键!)


指令重排的前提是,重排指令不能影响结果,例如

// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2


支持流水线的处理器


现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。(奔腾四(Pentium 4)支持高达 35 级流水线,但由于功耗太高被废弃)


SuperScalar 处理器


大多数处理器包含多个执行单元,并不是所有计算功能都集中在一起,可以再细分为整数运算单元、浮点数运算单元等,这样可以把多条指令也可以做到并行获取、译码等,CPU 可以在一个时钟周期内,执行多于一条指令,IPC>1


诡异的结果


  int num = 0;
    boolean ready = false;
    // 线程1 执行此方法
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    // 线程2 执行此方法
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

有一个属性 r1 用来保存结果,问,可能的结果有几种?


有同学这么分析


  • 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  • 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
  • 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)


但我告诉你,结果还有可能是 0 😁😁😁,信不信吧!


这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2


相信很多人已经晕了 😵😵(原因就是jvm对这 num 和 ready进行了指令重排序,那么结果可能是0)


这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:


借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -
DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -DartifactId=ordering -Dversion=1.0 

创建 maven 项目,提供如下测试类

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
    int num = 0;
    boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

执行

mvn clean install 
java -jar target/jcstress.jar

会输出我们感兴趣的结果,摘录其中一次结果:

  *** INTERESTING tests
        Some interesting behaviors observed. This is for the plain curiosity.
        2 matching test results.
        [OK] test.ConcurrencyTest
        (JVM args: [-XX:-TieredCompilation])
        Observed state Occurrences Expectation Interpretation
        0     1,729       ACCEPTABLE_INTERESTING !!!!
        1     42,617,915    ACCEPTABLE ok
        4     5,146,627     ACCEPTABLE ok
        [OK] test.ConcurrencyTest
        (JVM args: [])
        Observed state Occurrences Expectation Interpretation
        0     1,652       ACCEPTABLE_INTERESTING !!!!
        1     46,460,657    ACCEPTABLE ok
        4     4,571,072     ACCEPTABLE ok

可以看到,出现结果为 0 的情况有 一千多次,虽然次数相对很少,但毕竟是出现了。


解决方法


volatile 修饰的变量,可以禁用指令重排

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
@State
public class ConcurrencyTest {
    int num = 0;
    volatile boolean ready = false;
    @Actor
    public void actor1(I_Result r) {
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }
    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;// 在这上面加 volatile 能够防止 之前的代码被重排序
    }
}

结果为:

*** INTERESTING tests 
 Some interesting behaviors observed. This is for the plain curiosity. 
 0 matching test results.


volatile 原理


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


  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障


如何保证可见性


写屏障(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;
        }
    }


如何保证有序性


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


还是那句话,不能解决指令交错:


  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序


volatile底层的读写屏障只是 保证了可见性 和 有序性,还是不能解决指令交错


而synchronized都可以做到,有序 可见 原子


double-checked locking 问题


以著名的 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

其中


  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE


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


剑指JUC原理-8.Java内存模型(下):https://developer.aliyun.com/article/1413629

目录
相关文章
|
5天前
|
算法 调度 UED
深入理解操作系统内存管理:原理与实践
【4月更文挑战第23天】 在现代计算机系统中,操作系统的内存管理是保证系统高效、稳定运行的关键组成部分。本文旨在深入探讨操作系统中内存管理的理论基础、关键技术以及实际操作过程,通过对内存分配策略、虚拟内存技术、分页与分段机制等核心概念的详细解析,为读者提供一个清晰、全面的内存管理视角。此外,文章还将通过案例分析,展示内存管理在解决实际问题中的应用,以期加深读者对操作系统内存管理复杂性的认识和理解。
|
16天前
|
存储 Java 编译器
Java内存区域详解
Java内存区域详解
29 0
Java内存区域详解
|
22天前
|
Java 调度
Java并发编程:深入理解线程池的原理与实践
【4月更文挑战第6天】本文将深入探讨Java并发编程中的重要概念——线程池。我们将从线程池的基本原理入手,逐步解析其工作过程,以及如何在实际开发中合理使用线程池以提高程序性能。同时,我们还将关注线程池的一些高级特性,如自定义线程工厂、拒绝策略等,以帮助读者更好地掌握线程池的使用技巧。
|
22天前
|
缓存 安全 Java
Java并发编程进阶:深入理解Java内存模型
【4月更文挑战第6天】Java内存模型(JMM)是多线程编程的关键,定义了线程间共享变量读写的规则,确保数据一致性和可见性。主要包括原子性、可见性和有序性三大特性。Happens-Before原则规定操作顺序,内存屏障和锁则保障这些原则的实施。理解JMM和相关机制对于编写线程安全、高性能的Java并发程序至关重要。
|
1天前
|
设计模式 消息中间件 Java
Java 设计模式:探索发布-订阅模式的原理与应用
【4月更文挑战第27天】发布-订阅模式是一种消息传递范式,被广泛用于构建松散耦合的系统。在 Java 中,这种模式允许多个对象监听和响应感兴趣的事件。
8 2
|
4天前
|
Java 程序员 数据库连接
Java从入门到精通:3.3.2性能优化与调优——内存管理篇
Java从入门到精通:3.3.2性能优化与调优——内存管理篇
Java从入门到精通:3.3.2性能优化与调优——内存管理篇
|
5天前
|
存储 安全 Java
滚雪球学Java(19):JavaSE中的内存管理:你所不知道的秘密
【4月更文挑战第8天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
30 4
滚雪球学Java(19):JavaSE中的内存管理:你所不知道的秘密
|
12天前
|
存储 缓存 监控
Java内存管理:垃圾回收与内存泄漏
【4月更文挑战第16天】本文探讨了Java的内存管理机制,重点在于垃圾回收和内存泄漏。垃圾回收通过标记-清除过程回收无用对象,Java提供了多种GC类型,如Serial、Parallel、CMS和G1。内存泄漏导致内存无法释放,常见原因包括静态集合、监听器、内部类、未关闭资源和缓存。内存泄漏影响性能,可能导致应用崩溃。避免内存泄漏的策略包括代码审查、使用分析工具、合理设计和及时释放资源。理解这些原理对开发高性能Java应用至关重要。
|
17天前
|
运维 NoSQL 算法
Java开发-深入理解Redis Cluster的工作原理
综上所述,Redis Cluster通过数据分片、节点发现、主从复制、数据迁移、故障检测和客户端路由等机制,实现了一个分布式的、高可用的Redis解决方案。它允许数据分布在多个节点上,提供了自动故障转移和读写分离的功能,适用于需要大规模、高性能、高可用性的应用场景。
16 0
|
20天前
|
存储 算法
深入理解操作系统内存管理:原理与实践
【4月更文挑战第8天】 在现代计算机系统中,操作系统扮演着关键角色,特别是在内存资源的管理上。本文将深入探讨操作系统中的内存管理机制,包括虚拟内存、物理内存的分配与回收,以及页面置换算法等关键技术。通过分析不同内存管理策略的优势与局限性,本文旨在为读者提供一套系统的内存管理知识框架,帮助理解操作系统如何高效、安全地管理有限的内存资源以满足多任务处理的需求。