浅谈java内存模型

一、Java内存模型
1.定义

Java内存模型(JMM, Java Memory Model)是一个规范,是 Java语言设计者提供给 Java 开发者的理论支持以及正确的线程同步策略。

2.特性

屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各平台下都能达到一致的内存访问效果。
正确的进行多线程编程。可以让使用者正确的预测程序执行行为,写出正确的并发程序。

二、JSR133规范

早期java内存模型存在严重缺陷,难以进行正确的多线程编程。因此,从JDK5开始,Java采用了新的内存模型 JSR133。JSR133 为Java语言定义了一个新的内存模型,修复了旧版内存模型的缺陷。如今我们在谈java内存模型时,就是在讲JSR133规范的内容。

有兴趣深入了解的同学可以看下JSR文档:

图片

三、主内存与工作内存

Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,用于存储主内存中要使用的变量的副本。

线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存

不同的线程之间无法直接访问对方工作内存中的变量。

工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

Java内存模型抽象示意图如下所示:

图片

简单来说:一个变量会同时存在多个的数据副本。不仅在JVM主内存中存在一份数据,也可能在每个线程的工作内存中也可能存在一份数据。既然有多个副本,那Java程序员在进行多线程编程时,就需要注意可能存在的多个副本之间的数据一致性问题,即一个线程对变量进行修改后,其他线程并不一定能马上读到修改后的值。

四、指令重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

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

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

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

4.从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

图片

5.JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

6.java 编译器禁止处理器重排序是通过在生成指令序列的适当位置会插入内存屏障(重排序时不能把后面的指令重排序到内存屏障之前的位置)指令来实现的。

7.重排序需要遵守happens-before规则。(JSR133对happens-before有明确定义,这里不展开说明)

五、volatile关键字

volatile 可以说是 JVM 提供的最轻量级的同步机制,当一个变量定义为volatile之后,它将具备两种特性:

  1. 保证变量的可见性。当被volatile修饰的变量被修改时,其他线程能立刻读到被修改变量的最新值。

  2. 禁止指令重排序优化。普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。被volatile修饰的变量通过在读写指令附加内存屏障指令来禁止重排序,从而保证代码的执行顺序一致。

六、正确地多线程编程

在多线程的环境中,可见性、有序性以及原子性是构成线程安全的基石。

  • 可见性:

    普通代码:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程并不一定能马上看到修改的值。

    通过volatile修饰变量保证变量被修改后其他线程立即可见。

    Synchronized也能实现可见性:线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中。

  • 有序性:

    普通代码:通过happens-before规则保证代码的前后逻辑具备有序性。

    通过Volatile修饰变量禁止指令重排序严格保证代码的执行有序性。

  • 原子性:

    JMM 要求在没有任何的同步手段的前提下,变量的读写必须具备原子性。(但是允许了两个特例存在,非 volatile 修饰的 64位 double 和 long 类型。不同版本的 Java 可能有不同的处理方式,一般对 double 和 long 的处理方式是,将一个 64 位拆分成两个 32 位分别原子性读写,这样一来多线程环境下就有问题了,很可能高32位和低32位分别被两个不同线程读写,出现诡异问题。目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此在编写代码时一般也不需要将用到的long和double变量专门声明为volatile)。


    代码实例:

    i=50 。该代码对变量i进行写操作,是一个原子操作

    i++。这个操作不是原子操作(涵盖了三个操作:load变量i,自增,store 变量i)。

    要严格保证原子性,需要在volatile修饰的基础上,使用同步块技术(锁)和Java concurrent包(原子操作类等)。

七、为什么要抽象出工作内存

为什么java的线程和主内存之间不能互相访问,而是需要在中间加一个工作内存?

在现代硬件内存模型中,由于CPU运算速度和访问内存的速度有几个数量级的差距,为了避免处理器等待缓慢的内存完成读写操作,现代计算机系统通过加入一层读写速度尽可能接近处理器运算速度的高速缓存。简单描述CPU的结构如下图所示:

图片

我们可以发现,该图和上面的java内存模型抽象示意图非常相似。所以java内存模型是对底层硬件的内存处理逻辑进行高度抽象,它是一个规范,并非真实的存在。

中…(img-611vG5w9-1622113206886)]

我们可以发现,该图和上面的java内存模型抽象示意图非常相似。所以java内存模型是对底层硬件的内存处理逻辑进行高度抽象,它是一个规范,并非真实的存在。

当然,真实的CPU结构远比上图复杂。下篇将会进行更详细的说明,我们下期再见!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值