首页 > 系统相关 >五、JUC-Java内存模型JMM

五、JUC-Java内存模型JMM

时间:2023-05-26 19:07:08浏览次数:39  
标签:JUC Java 变量 线程 内存 JMM 操作 before

一、一些疑问

  • 什么是Java内存模型JMM
  • JMM与volatile他们两个之间的关系
  • JMM有哪些特性
  • 为什么要有JMM,他们为什么出现,功能和作用是什么
  • happens-before先行发生原则是什么

二、计算机硬件存储体系

计算机存储结构,从本地磁盘到主存到CPU缓存,也就是从硬盘到内存到CPU 一般对应的程序的操作就是从数据库查询数据到内存然后到CPU进行计算 91.png 92.png

  • 因为有这么多级的缓存(cpu和物理主内存的速度不一致的),CPU的运行并不是直接操作内存而是先把内存里的数据读到缓存,而欸村的读和写操作的时候就会造成不一致的问题
  • java规范中试图定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

94.png

三、JMM

1、JMM概念

JMM(Java内存模型)本身是一种抽象的概念,并不是真实存在的,它仅仅描述的是一组约定或者是规范,通过这组规范定义了程序中各个变量的读写访问方式,并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见。关键技术点都是围绕多线程的原子性、可见性和有序性展开的

2、原则

JMM的关键技术点都是围绕多线程的原子性,可见性,和有序性展开的。

3、能干啥?

  • 通过JMM来实现线程和主内存之间的抽象关系
  • 屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

4、JMM的三大特性

1、可见性

是指当一个线程修改了某一个共享变量值,其他线程是否能够立即知道该变更,JMM规定了所有的变量都存储在内存中 95.png

系统主内存共享变量数据修改被写入的时机是不确定的,在多线程并发下很可能出现脏读,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存拷贝副本,线程对变量的所有操作都必须在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

96.png

主内存中有变量x,初始值为0 线程A要将X+1,要先将x=0拷贝到自己的私有内存中,然后更新x的值 线程A将更新后的X的值1刷回到主内存的时间点是不确定的 刚好线程A还没有将最新的值刷回到主内存时,线程B同样从主内存中读取X,此时值为0,和线程A一样的操作,最后期盼X=2,就会成为X=1

2、原子性

3、有序性

概念 对于一个线程执行代码而言,我们总是习惯的认为,代码的执行总是从上到下的有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序,Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码的顺序不一致,此过程叫指令的重排序。 优缺点 JVM能根据处理器特性适当的对指令进行重新排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能,但是,指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致。简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。 从源码到最终执行示例图: 97.png 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致 处理器在进行重排序时,必须要考虑指令间的数据依赖性 多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

5、JMM规范下,多线程对变量的读写过程

读取过程: 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到的线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程问无法访问对方的工作内存,线程问的通信(传值)必须通过主内存来完成,其简要访问过程如下图: 98.png JMM定义了线程和主内存之间的抽象关系

  • 线程之间的共享变量存储在主内存中(从硬件角度来说就是内存条
  • 每个线程都有一个私有的本地工作内存,本地工作内存中存储了该线程用来读/写共享变量的副本(从硬件角度来说就是CPU的缓存,比如寄存器、L1、L2、L3缓存等)

总结:

  • 我们定义的所有共享变量都储存在物理主内存中 0 每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
  • 线程对共享变量所有的操作都必须先在线程自己的工作内存中进行后写回主内存,不能直按从主内存中读写(不能越级)
  • 不同线程之间也无法直按访问其他线程的工作内存中的变量,线程间变量值的传递需要通过主内存来进行(同级不能相互访问)

6、JMM规范下,多线程先行发生原则之happens-before

在JMM中,如果一个操作执行的结果盄要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则。 逻辑上的先启关系 X、Y案例说明: 99.png 先行发生原则说明:

  • 如果Java内存模型中所有的有序性都仅靠volatile和synchronized来完成,那么有很多操作都将会变得非常啰嗦,但是我们在编写Java并发代码的时候并没有察觉到这一点。
  • 我们没有时时、处处、次次,添加volatile 和synchronized来完成程序,这是因为Java语言中JMM原则下有一个“先行发生” (Happens-Before)的原则限制和规矩,给你立好了规矩!
  • 这个原则非常重要:
    • 它是判断数据是否存在竞争,线程是否安全的非常有用的手段。依赖这个原则,我们可以通过几条简单规则一揽子解决并发环境下两个操作之问是否可能存在冲突的所有问题,而不需要陷入Java内存模型苦涩难懂的底层编译原理之中。

happens-before总原则

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
  • 两个操作之间存在happens-before 关系,并不意味者一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来我行的结東一致,那么这种重排序并不非法。

happens-before官方八条原则:

  • 次序规则:一个线程内,按照代码顺序,写在前面的操作哦先行发生于写在后面的操作。
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的"后面”同样是指时间上的先后。
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start方法先行发生于此线程的每一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;可以通过Thread.interrupted()检测到是否发生中断也就是说你要先调用interrupt()方法设置过中断标志位,我才能检测到中断发送
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过isAlive()等手段检测线程是否已经终止执行。
  • 对象终结规则:一个对象的初始化完成,先行发生于它的finalize()方法的开始。也就是说,对象没有完成初始化之前,是不能调用finalize()方法的。

小总结:

  • 在Java 语言里面,Happens-Before 的语义本质上是一种可见性
  • A Happens-Before B 意味着 A发生过的事情对B来说是可见的,无论 A事件和B事件是否发生在同一个线程里.
  • JMM的设计分为两部分:
    • 一部分是面向我们程序 员提供的,也就是happens-before规则,它通俗易懂的向我们程序 员闸述了一个强内存模型,我们只要理解happens-before规则,就可以编写并发安全的程序了。

    • 另一部分是针对JVM实现的,为了尽可能少的对编译器和处理器做约束从而提高性能,JMM在不影响程序执行结果的前提下对其不做要求,即允许优化重排序。我们只需要关注前者就好了,也就是理解happens-before规则即可,其它繁朵的内容有JMM帆范结合操作 系统给我们搞定,我们只写好代码即可。

问题: 100.png 解决方案1,比较重,不推荐 101.png 解决方案2 102.png

标签:JUC,Java,变量,线程,内存,JMM,操作,before
From: https://blog.51cto.com/u_15410237/6358225

相关文章

  • Java大文件分片上传/多线程上传方案
    ​ 在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现。先说下要求:PC端全平台支持,要求支持Windows,Mac,Linux支持所有浏览器。支持文件批量上传支持文件夹上传,且要求在服务端保留层级结构。文件夹数量要求支持到10W......
  • 学习日记——Java循环结构
    1.while循环①语法:while循环语法//参数的初始化inti=1;//循环条件while(i<10){//循环操作System.out.println("这是测试");//变更参数i++;}特点:先判断,再执行②实战案例:2.dowhile循环①语法:dowhile语法 //初始化参数inti=0;do{......
  • java怎么打印一个对象的内存地址
    在Java一般使用HashCode来代表对象的地址,但是两个相同的对象就不行了,两个相同的对象的hashcode是相同的。  如果要对比两个相同的对象的地址可以使用,System.identityHashCode(传输进去的参数是一个对象)......
  • Java笔记(十):函数式接口
    函数式接口有且仅有一个抽象方法的接口JDK8中,只有一个抽象方法的接口称为函数式接口,我们就能使用Lambda。针对一个接口中,是否有大于一个抽象方法?JDK8为我们新增了一个注解:@FunctionalInterface。它能够帮助我们检测这个接口是不是只有一个抽象方法,如果有两个抽象方法,则会报......
  • Java大文件分片上传/多线程上传实例解析
    ​ javaweb上传文件上传文件的jsp中的部分上传文件同样可以使用form表单向后端发请求,也可以使用ajax向后端发请求    1. 通过form表单向后端发送请求         <formid="postForm" action="${pageContext.request.contextPath}/UploadServlet" method="pos......
  • Java笔记(九):线程池
    三大方法Executors.newSingleThreadExecutor();//单个线程Executors.newFixedThreadPool(5);//固定的线程池大小Executors.newCachedThreadPool();//可伸缩的以上底层都是由ThreadPoolExecutor实现阿里开发手册:线程池不允许使用Executors去创建,而是通过ThreadP......
  • 基于JAVA的虚拟页面置换算法
    访问【WRITE-BUG数字空间】_[内附完整源码和文档]一概述目的:实现OPT、FIFO、LRU三种置换算法。主要完成任务:实现三种算法并计算出每种算法的却页数,且以表格形式输出结果。使用开发工具:Eclipse—Java。解决主要问题:编写出可视化窗口输出运行结果。二使用的基本概念和原理OPT:在分配......
  • Java大文件分片上传/多线程上传源代码
    ​ 这里只写后端的代码,基本的思想就是,前端将文件分片,然后每次访问上传接口的时候,向后端传入参数:当前为第几块文件,和分片总数下面直接贴代码吧,一些难懂的我大部分都加上注释了:上传文件实体类:看得出来,实体类中已经有很多我们需要的功能了,还有实用的属性。如MD5秒传的信息。pub......
  • Java第四日
    今日份Java学习内容循环缘由与定义while循环结构调试程序do-while循环结构for循环  1.  循环缘由与定义     核心要点:在某种场景(多次重复做同一件事的情况)需要使用循环结构,特点:提高效率。 循环结构特点:1.循环条件(初始值、结束值)       ......
  • Java订单号生成唯一
    公司需要开发商城项目,用到了,记录一下Java订单号生成唯一packagecom.iot.store.utils;importorg.apache.commons.lang3.RandomStringUtils;importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;publicclassOrderIDGenerator{publicstati......