JMM之有序性介绍

简介: JMM之有序性介绍

一、基本概念

对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。

但为了提升性能,编译器和处理器通常会对指令序列进行重新排序

Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果一致,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序

指令重排序类型

(1)编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

(2)指令级并行的重排序:现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

(3)内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题

指令重排序优缺点

  • 优点:JVM能根据处理器特性(CPU多级缓存系统、多核处理等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度发挥机器性能。
  • 缺点:指令重排序可以保证串行语义一致,但没有义务保证多线程之间的语义一致(即可能产生“脏读”)。

二、代码解读指令重排

JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

i = ...;
j = ...;

也可以是

j = ...;
i = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU 执行指令的原理来理解一下吧

三、指令级并行原理

指令重排序优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回这 5 个阶段。

术语参考:


instruction fetch (IF)


instruction decode (ID)


execute (EX)


memory access (MEM)


register write back (WB)

在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,分阶段、分工正是提升效率的关键!

支持流水线的处理器

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

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

四、指令重排序带来的问题

4.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;
}

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

  • 情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  • 情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
  • 情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
  • 情况4:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加结果为 0,再切回线程2 执行 num = 2(情况4由于指令重排序出现里问题)

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化

  • 这个现象需要通过大量测试才能复现:借助 java 并发压测工具jcstress(Java Concurrency Stress)
  • jmeter侧重对于接口整体的响应速度等进行测试,而JCStress框架能对某块逻辑代码进行高并发测试,更加侧重JVM,类库等领域的研究

4.2 指令重排的说明

  • 指令重排序操作不会对存在数据依赖关系的操作进行重排序。比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。 比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系,所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

指令重排序在单线程模式下是一定会保证最终结果的正确性, 但是在多线程环境下,问题就出来了。

五、解决方法

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

注:对于volatile禁止指令重排的原理见文章:volatile原理

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.I_Result;
@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;//加上 volatile 防止 修改ready操作 之前的写指令重排
    @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;
    }
}

运行结果

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

思考:是否可以通过synchronized来解决该问题?

使用synchronized并不能解决有序性问题,但是如果是该变量整个都在synchronized代码块的保护范围内,那么变量就不会被多个线程同时操作,也不用考虑有序性问题!在这种情况下相当于解决了重排序问题!

目录
相关文章
|
3月前
|
缓存 安全 Java
简单了解下JMM解决什么问题
Java内存模型(JMM)是Java语言规范的一部分。JMM通过“happens-before”规则和内存屏障等机制,确保在多线程程序中,各线程对共享变量的操作行为符合预期。
43 3
|
4月前
|
安全 Java 开发者
探索Java内存模型:可见性、有序性和并发
在Java的并发编程领域中,内存模型扮演了至关重要的角色。本文旨在深入探讨Java内存模型的核心概念,包括可见性、有序性和它们对并发实践的影响。我们将通过具体示例和底层原理分析,揭示这些概念如何协同工作以确保跨线程操作的正确性,并指导开发者编写高效且线程安全的代码。
|
5月前
|
存储 缓存 Java
深入理解JMM
深入理解JMM
121 2
|
5月前
|
Java
Java内存模型之原子性问题
Java内存模型之原子性问题
|
存储 缓存 SpringCloudAlibaba
JUC并发编程(一):Java内存模型(JMM)及三大特性:可见性、有序性、原子性
在当今高流量、高并发的互联网业务场景下,**并发编程技术**显得尤为重要,不管是哪一门编程语言,掌握并发编程技术是个人进阶的必经之路。时隔一个半月没有写技术博客文章,有点生疏了。。。闲话少叙,接下来我将围绕并发编程知识点进行总结讲解,这里从并发编程入门开始,讲述Java内存模型和并发的三大特性。
188 1
JUC并发编程(一):Java内存模型(JMM)及三大特性:可见性、有序性、原子性
|
Java 编译器 程序员
JMM的内存可见性保证
JMM的内存可见性保证
52 0
|
缓存 Java 编译器
05.深入理解JMM和Happens-Before
大家好,我是王有志。今天我们一起来学习Java并发编程中最重要的两个理论知识JMM和Happens-Before原则。
121 1
05.深入理解JMM和Happens-Before
|
存储 缓存 Java
到底什么是内存可见性?
到底什么是内存可见性?
147 0
|
存储 缓存 Java
关于JMM的理解
JMM是java内存模型,它描述的是和多线程相关的一组规范。通过这组规范定义了程序中对各个变量的访问方式。保证了不同jvm运行并发程序的结果的一致性和可靠性。
106 0
|
缓存 Java 编译器
并发编程(三)原子性&可见性&有序性
并发编程(三)原子性&可见性&有序性
121 0