JMM内存模型 volatile关键字解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: JMM内存模型 volatile关键字解析

对于多线程等等的各种操作,相比各位都了然于胸,现在我们来介绍一下更底层一点点的JMM内存模型,其实也是一个很简单的理想的内存模型

注意与JVM的内存模型区分

多线程内存模型主要是基于CPU缓存搭建起来的

这里就区分工作内存和主内存了

我们线程操作的其实是主内存的一个副本,多线程每个线程操作结束了之后需要刷回主内存

volatile关键字

我们知道volatile主要的两个功能就是

1.保证内存中的变量可见性

2.禁止指令重排序

下面我们来介绍一些关于volatile关键字以及高并发的内容

我们知道,如果两个线程同时操作一个变量,我们这里就称之为线程a和线程b,a线程假设操作了主内存的一个变量,另一个线程能感知到吗?

答案是不能,因为两者始终操作的是其工作内存中的变量副本而已.

如果这里我给共享的变量加入一个volatile关键字修饰,这里就b线程就能感知到工作内存中的变量变化了,这是为什么呢?

请听我慢慢解释

首先我们需要先了解一下经典的原子操作

然后我们来谈谈另一个线程是怎么感知到的

说到这里就不得不提我们的MESI(缓存一致性)协议了

这里就是线程a一修改主内存中的变量,其实这个修改是通过总线传输到主内存的

这里线程b就是通过总线嗅探机制,一直在监听总线,当他发现这里总线中有我的属性被修改了

这里我们的b线程对应的变量的属性就直接设置为I(无效),当下次线程b需要使用这个变量的时候

哦,他就只能取主内存里面再去刷入工作内存了

那么我们的硬件协议又是怎么让缓存一致性协议生效的呢?

其本质就是在其底层的汇编代码前面加上了lock前缀

注意这里是修改完直接就同步回主内存,主打一个即时性

关于这里的指令重排序,我们也来谈一谈

为什么指令重排序这里的指令重排序

主要是为了加快程序性能而产生的

遵循 happens before 和 as is serial 原则

本质上就是在下面一条语句依赖于上面一条语句的时候

不会执行指令重排序,不影响依赖关系就随便排序

注:java 在执行代码之前会看看语法树前后有没有相互依赖

下面我展示部分原则

懒汉模式出现的对象半初始化问题

我们知道懒汉模式这里会使用双重校验锁

我拿出了其两行字节码指令

假设这里的putstatic在init之前

这里刚刚putstatic之后,cpu就调度到另一个线程了

这里判断已经不是空了,直接拿来使用,就会发生意想不到的问题

假设我这里a开了一个账户充6000块,然后直接去消费了

结果消费的时候发现账户的前不翼而飞了,所以这里的指令重排序问题是一个大的问题

常见的几种内存屏障

我们都知道volatile关键字是底层实现其实是一些内存屏障来实现的

我这里贴出几种常见的内存屏障

然后我们可以查看一下Java具体是怎么实现的

我们以 openjdk8 根路径 jdk\src\hotspot\share\interpreter\zero 路径下的 bytecodeInterpreter.cpp 文件中,处理 putstatic 指令的代码:

先进行判断是否有volatile修饰的实例

然后判断是对于不同的修饰类型进行操作

CASE(_putstatic):
    {
          // .... 省略若干行 
          // Now store the result 现在要开始存储结果了
          // ConstantPoolCacheEntry* cache;     -- cache是常量池缓存实例
          // cache->is_volatile()               -- 判断是否有volatile访问标志修饰
          int field_offset = cache->f2_as_index();
          // ****重点判断逻辑**** 
          if (cache->is_volatile()) { 
            // volatile变量的赋值逻辑
            if (tos_type == itos) {
              obj->release_int_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == atos) {// 对象类型赋值
              VERIFY_OOP(STACK_OBJECT(-1));
              obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
              OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
            } else if (tos_type == btos) {// byte类型赋值
              obj->release_byte_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ltos) {// long类型赋值
              obj->release_long_field_put(field_offset, STACK_LONG(-1));
            } else if (tos_type == ctos) {// char类型赋值
              obj->release_char_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == stos) {// short类型赋值
              obj->release_short_field_put(field_offset, STACK_INT(-1));
            } else if (tos_type == ftos) {// float类型赋值
              obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
            } else {// double类型赋值
              obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
            }
            // *** 写完值后的storeload屏障 ***
            OrderAccess::storeload();
          } else {
            // 非volatile变量的赋值逻辑
          }       
  }

我们看到这里判断完的的storeload

然后我们介绍一下这个fence函数

这里先判断使用的显卡还是其他显卡

其实没有什么区别,主要是amd使用rsp,其他显卡使用的是esp,使用的寄存器不同

inline void OrderAccess::fence() {
   // always use locked addl since mfence is sometimes expensive
   // 
#ifdef AMD64
  __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
  __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  compiler_barrier();
}

这个__asm__就是表示告诉编译器在这里插入汇编代码

volatile就是告诉编译器我这里插入的汇编代码原原本本的给我执行,不许重排序

我们发现这些名字前面都有一个lock就是会将这块内存区域的缓存锁定并写回到主内存中

相关文章
|
2月前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
28天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
2月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
2月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
27天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
37 2
|
2月前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
2月前
|
存储 安全 Java
JVM锁的膨胀过程与锁内存变化解析
在Java虚拟机(JVM)中,锁机制是确保多线程环境下数据一致性和线程安全的重要手段。随着线程对共享资源的竞争程度不同,JVM中的锁会经历从低级到高级的膨胀过程,以适应不同的并发场景。本文将深入探讨JVM锁的膨胀过程,以及锁在内存中的变化。
44 1
|
3月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
120 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
3月前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理策略和垃圾回收机制。首先介绍了Java内存模型的基本概念,包括堆、栈以及方法区的划分和各自的功能。进一步详细阐述了垃圾回收的基本原理、常见算法(如标记-清除、复制、标记-整理等),以及如何通过JVM参数调优垃圾回收器的性能。此外,还讨论了Java 9引入的接口变化对垃圾回收的影响,以及如何通过Shenandoah等现代垃圾回收器提升应用性能。最后,提供了一些编写高效Java代码的实践建议,帮助开发者更好地理解和管理Java应用的内存使用。
41 3

推荐镜像

更多