精华内容
下载资源
问答
  • 主要介绍了Java内存模型与JVM运行时数据区的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • Java 内存模型

    2018-07-23 16:45:07
    深入理解 java 内存模型是 java 程序员的必修课,看看原汁原味正宗的内存模型
  • 基础 4 并发编程模型的分类 4 Java 内存模型的抽象 4 ... JMM,处理器内存模型与顺序一致性内存模型之间的关系 68 JMM 的设计 69 JMM 的内存可见性保证 72 JSR-133 对旧内存模型的修补 73 个人简介 74 参考文献 74
  • 先来看一下 CPU 的内存模型 JVM内存模型 java内存模型逻辑抽象图 java 内存模型 lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态 unlock(解锁):作用于主内存的变量,把一个处于锁定状态的...
  • 局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的...
  • Java 8 内存模型.pdf

    2018-03-22 13:50:36
    java内存模型介绍java内存模型介绍java内存模型介绍java内存模型介绍java内存模型介绍
  • jvm详细内存模型图1

    2018-09-03 20:35:25
    查看《深入理解Java虚拟机》后,自己简单总结的jvm相关的简单模型图。
  • 深入理解Java内存模型 pdf 超清版
  • 讲一讲什么是Java内存模型 Java内存模型虽说是一个老生常谈的问题 ,也是大厂面试中绕不过的,甚至初级面试也会问到。但是真正要理解起来,还是相当困难,主要这个东西看不见,摸不着。 这是一个比较开放的题目,...
  • 主要介绍了javascript 内存模型,结合实例形式详细分析了javascript 内存模型相关概念、原理、操作技巧与注意事项,需要的朋友可以参考下
  • 深度剖析java内存模型

    2019-06-03 11:35:21
    深度剖析java内存模型深度剖析java内存模型深度剖析java内存模型
  • Java内存模型的历史变迁 发表于2015-05-20 11:00|?1970次阅读| 来源CSDN|?2?条评论| 作者程晓明 特别策划 Java Java20周年 程序员电子刊 摘要本文通过介绍Java的新/旧内存模型来展示Java技术的历史变迁 本文通过介绍...
  • 详细介绍Java内存,ava线程之间的通信对程序员完全透明,内存可见性问题很容易困扰java程序员,本文试图揭开java内存模型神秘的面纱。本文大致分三部分:重排序与顺序一致性;三个同步原语(lock,volatile,final)...
  • JVM内存模型详解

    2019-02-22 14:48:33
    jvm内存模型,jvm脑图,jvm调优,jvm垃圾回收算法,jvm垃圾回收器,逃逸算法等总结。
  • 全面理解Java内存模型(JMM)及volatile关键字

    万次阅读 多人点赞 2017-06-12 11:25:05
    本篇主要结合博主个人对Java内存模型的理解以及相关书籍内容的分析作为前提,对JMM进行较为全面的分析,本篇的写作思路是先阐明Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系,在...

    【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
    http://blog.csdn.net/javazejian/article/details/72772461
    出自【zejian的博客】

    关联文章:

    深入理解Java类型信息(Class对象)与反射机制

    深入理解Java枚举类型(enum)

    深入理解Java注解类型(@Annotation)

    深入理解Java类加载器(ClassLoader)

    深入理解Java并发之synchronized实现原理

    Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

    深入理解Java内存模型(JMM)及volatile关键字

    剖析基于并发AQS的重入锁(ReetrantLock)及其Condition实现原理

    剖析基于并发AQS的共享锁的实现(基于信号量Semaphore)

    并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue

    本篇主要结合博主个人对Java内存模型的理解以及相关书籍内容的分析作为前提,对JMM进行较为全面的分析,本篇的写作思路是先阐明Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系,在弄明白它们间的关系后,进一步分析Java内存模型作用以及一些必要的实现手段,以下是本篇主要内容(如有错误,欢迎留言,谢谢!)

    理解Java内存区域与Java内存模型

    Java内存区域

    Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁的时机,其中蓝色部分代表的是所有线程共享的数据区域,而绿色部分代表的是每个线程的私有数据区域。

    • 方法区(Method Area):

      方法区属于线程共享的内存区域,又称Non-Heap(非堆),主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。值得注意的是在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。

    • JVM堆(Java Heap):

      Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

    • 程序计数器(Program Counter Register):

      属于线程私有的数据区域,是一小块内存空间,主要代表当前线程所执行的字节码行号指示器。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

    • 虚拟机栈(Java Virtual Machine Stacks):

      属于线程私有的数据区域,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程,如下(图有误,应该为栈桢):

    • 本地方法栈(Native Method Stacks):

      本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关,一般情况下,我们无需关心此区域。

    这里之所以简要说明这部分内容,注意是为了区别Java内存模型与Java内存区域的划分,毕竟这两种划分是属于不同层次的概念。

    Java内存模型概述

    Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图

    需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的(稍后会分析)。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。关于JMM中的主内存和工作内存说明如下

    • 主内存

      主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

    • 工作内存

      主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

    弄清楚主内存和工作内存后,接了解一下主内存与工作内存的数据存储类型以及操作方式,根据虚拟机规范,对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存,简单示意图如下所示:

    硬件内存架构与Java内存模型

    硬件内存架构

    正如上图所示,经过简化CPU与内存操作的简易图,实际上没有这么简单,这里为了理解方便,我们省去了南北桥并将三级缓存统一为CPU缓存(有些CPU只有二级缓存,有些CPU有三级缓存)。就目前计算机而言,一般拥有多个CPU并且每个CPU可能存在多个核心,多核是指在一枚处理器(CPU)中集成两个或多个完整的计算引擎(内核),这样就可以支持多任务并行执行,从多线程的调度来说,每个线程都会映射到各个CPU核心中并行运行。在CPU内部有一组CPU寄存器,寄存器是cpu直接访问和处理的数据,是一个临时放数据的空间。一般CPU都会从内存取数据到寄存器,然后进行处理,但由于内存的处理速度远远低于CPU,导致CPU在处理指令时往往花费很多时间在等待内存做准备工作,于是在寄存器和主内存间添加了CPU缓存,CPU缓存比较小,但访问速度比主内存快得多,如果CPU总是操作主内存中的同一址地的数据,很容易影响CPU执行速度,此时CPU缓存就可以把从内存提取的数据暂时保存起来,如果寄存器要取内存中同一位置的数据,直接从缓存中提取,无需直接从主内存取。需要注意的是,寄存器并不每次数据都可以从缓存中取得数据,万一不是同一个内存地址中的数据,那寄存器还必须直接绕过缓存从内存中取数据。所以并不每次都得到缓存中取数据,这种现象有个专业的名称叫做缓存的命中率,从缓存中取就命中,不从缓存中取从内存中取,就没命中,可见缓存命中率的高低也会影响CPU执行性能,这就是CPU、缓存以及主内存间的简要交互过程,总而言之当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存(当然如果CPU缓存中存在需要的数据就会直接从缓存获取),进而在读取CPU缓存到寄存器,当CPU需要写数据到主存时,同样会先刷新寄存器中的数据到CPU缓存,然后再把数据刷新到主内存中。

    Java线程与硬件处理器

    了解完硬件的内存架构后,接着了解JVM中线程的实现原理,理解线程的实现原理,有助于我们了解Java内存模型与硬件内存架构的关系,在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。这里需要了解一个术语,内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。如下图

    如图所示,每个线程最终都会映射到CPU中进行处理,如果CPU存在多核,那么一个CPU将可以并行执行多个线程任务。

    Java内存模型与硬件内存架构的关系

    通过对前面的硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。(注意对于Java内存区域划分也是同样的道理)

    JMM存在的必要性

    在明白了Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系后,接着来谈谈Java内存模型存在的必要性。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。如下图,主内存中存在一个共享变量x,现在有A和B两条线程分别对该变量x=1进行操作,A/B线程各自的工作内存中存在共享变量副本x。假设现在A线程想要修改x的值为2,而B线程却想要读取x的值,那么B线程读取到的值是A线程更新后的值2还是更新前的值1呢?答案是,不确定,即B线程有可能读取到A线程更新前的值1,也有可能读取到A线程更新后的值2,这是因为工作内存是每个线程私有的数据区域,而线程A变量x时,首先是将变量从主内存拷贝到A线程的工作内存中,然后对变量进行操作,操作完成后再将变量x写回主内,而对于B线程的也是类似的,这样就有可能造成主内存与工作内存间数据存在一致性问题,假如A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1拷贝到自己的工作内存中,这样B线程读取到的值就是x=1,但如果A线程已将x=2写回主内存后,B线程才开始读取的话,那么此时B线程读取到的就是x=2,但到底是哪种情况先发生呢?这是不确定的,这也就是所谓的线程安全问题。

    为了解决类似上述的问题,JVM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则也称为Java内存模型(即JMM),JMM是围绕着程序执行的原子性、有序性、可见性展开的,下面我们看看这三个特性。

    Java内存模型的承诺

    这里我们先来了解几个概念,即原子性?可见性?有序性?最后再阐明JMM是如何保证这3个特性。

    原子性

    原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。有点要注意的是,对于32位系统的来说,long类型数据和double类型数据(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作),它们的读写并非原子性的,也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元,这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。但也不必太担心,因为读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。

    理解指令重排

    计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种

    • 编译器优化的重排

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

    • 指令并行的重排

      现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序

    • 内存系统的重排

      由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

    其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题,下面分别阐明这两种重排优化可能带来的问题

    编译器重排

    下面我们简单看一个编译器重排的例子:

    线程 1             线程 2
    1: x2 = a ;      3: x1 = b ;
    2: b = 1;         4: a = 2 ;

    两个线程同时执行,分别有1、2、3、4四段执行代码,其中1、2属于线程1 , 3、4属于线程2 ,从程序的执行顺序上看,似乎不太可能出现x1 = 1 和x2 = 2 的情况,但实际上这种情况是有可能发现的,因为如果编译器对这段程序代码执行重排优化后,可能出现下列情况

    线程 1              线程 2
    2: b = 1;          4: a = 2 ; 
    1:x2 = a ;        3: x1 = b ;

    这种执行顺序下就有可能出现x1 = 1 和x2 = 2 的情况,这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

    处理器指令重排

    先了解一下指令重排的概念,处理器指令重排是对CPU的性能优化,从指令的执行角度来说一条指令可以分为多个步骤完成,如下

    • 取指 IF
    • 译码和取寄存器操作数 ID
    • 执行或者有效地址计算 EX
    • 存储器访问 MEM
    • 写回 WB

    CPU在工作时,需要将上述指令分为多个步骤依次执行(注意硬件不同有可能不一样),由于每一个步会使用到不同的硬件操作,比如取指时会只有PC寄存器和存储器,译码时会执行到指令寄存器组,执行时会执行ALU(算术逻辑单元)、写回时使用到寄存器组。为了提高硬件利用率,CPU指令是按流水线技术来执行的,如下:

    从图中可以看出当指令1还未执行完成时,第2条指令便利用空闲的硬件开始执行,这样做是有好处的,如果每个步骤花费1ms,那么如果第2条指令需要等待第1条指令执行完成后再执行的话,则需要等待5ms,但如果使用流水线技术的话,指令2只需等待1ms就可以开始执行了,这样就能大大提升CPU的执行性能。虽然流水线技术可以大大提升CPU的性能,但不幸的是一旦出现流水中断,所有硬件设备将会进入一轮停顿期,当再次弥补中断点可能需要几个周期,这样性能损失也会很大,就好比工厂组装手机的流水线,一旦某个零件组装中断,那么该零件往后的工人都有可能进入一轮或者几轮等待组装零件的过程。因此我们需要尽量阻止指令中断的情况,指令重排就是其中一种优化中断的手段,我们通过一个例子来阐明指令重排是如何阻止流水线技术中断的

    a = b + c ;
    d = e + f ;

    下面通过汇编指令展示了上述代码在CPU执行的处理过程

    • LW指令 表示 load,其中LW R1,b表示把b的值加载到寄存器R1中
    • LW R2,c 表示把c的值加载到寄存器R2中
    • ADD 指令表示加法,把R1 、R2的值相加,并存入R3寄存器中。
    • SW 表示 store 即将 R3寄存器的值保持到变量a中
    • LW R4,e 表示把e的值加载到寄存器R4中
    • LW R5,f 表示把f的值加载到寄存器R5中
    • SUB 指令表示减法,把R4 、R5的值相减,并存入R6寄存器中。
    • SW d,R6 表示将R6寄存器的值保持到变量d中

    上述便是汇编指令的执行过程,在某些指令上存在X的标志,X代表中断的含义,也就是只要有X的地方就会导致指令流水线技术停顿,同时也会影响后续指令的执行,可能需要经过1个或几个指令周期才可能恢复正常,那为什么停顿呢?这是因为部分数据还没准备好,如执行ADD指令时,需要使用到前面指令的数据R1,R2,而此时R2的MEM操作没有完成,即未拷贝到存储器中,这样加法计算就无法进行,必须等到MEM操作完成后才能执行,也就因此而停顿了,其他指令也是类似的情况。前面阐述过,停顿会造成CPU性能下降,因此我们应该想办法消除这些停顿,这时就需要使用到指令重排了,如下图,既然ADD指令需要等待,那我们就利用等待的时间做些别的事情,如把LW R4,eLW R5,f 移动到前面执行,毕竟LW R4,eLW R5,f执行并没有数据依赖关系,对他们有数据依赖关系的SUB R6,R5,R4指令在R4,R5加载完成后才执行的,没有影响,过程如下:

    正如上图所示,所有的停顿都完美消除了,指令流水线也无需中断了,这样CPU的性能也能带来很好的提升,这就是处理器指令重排的作用。关于编译器重排以及指令重排(这两种重排我们后面统一称为指令重排)相关内容已阐述清晰了,我们必须意识到对于单线程而已指令重排几乎不会带来任何影响,比竟重排的前提是保证串行语义执行的一致性,但对于多线程环境而已,指令重排就可能导致严重的程序轮序执行问题,如下

    class MixedOrder{
        int a = 0;
        boolean flag = false;
        public void writer(){
            a = 1;
            flag = true;
        }
    
        public void read(){
            if(flag){
                int i = a + 1;
            }
        }
    }

    如上述代码,同时存在线程A和线程B对该实例对象进行操作,其中A线程调用写入方法,而B线程调用读取方法,由于指令重排等原因,可能导致程序执行顺序变为如下:

     线程A                    线程B
     writer:                 read:
     1:flag = true;           1:flag = true;
     2:a = 1;                 2: a = 0 ; //误读
                              3: i = 1 ;

    由于指令重排的原因,线程A的flag置为true被提前执行了,而a赋值为1的程序还未执行完,此时线程B,恰好读取flag的值为true,直接获取a的值(此时B线程并不知道a为0)并执行i赋值操作,结果i的值为1,而不是预期的2,这就是多线程环境下,指令重排导致的程序乱序执行的结果。因此,请记住,指令重排只会保证单线程中串行语义的执行的一致性,但并不会关心多线程间的语义一致性。

    可见性

    理解了指令重排现象后,可见性容易了,可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。

    有序性

    有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。

    JMM提供的解决方案

    在理解了原子性,可见性以及有序性问题后,看看JMM是如何保证的,在Java内存模型中都提供一套解决方案供Java工程师在开发过程使用,如原子性问题,除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性,关于synchronized的详解,看博主另外一篇文章( 深入理解Java并发之synchronized实现原理)。而工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化,关于volatile稍后会进一步分析。除了靠sychronized和volatile关键字来保证原子性、可见性以及有序性外,JMM内部还定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。

    理解JMM中的happens-before 原则

    倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下

    • 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

    • 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

    • volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

    • 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见

    • 传递性 A先于B ,B先于C 那么A必然先于C

    • 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。

    • 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。

    • 对象终结规则 对象的构造函数执行,结束先于finalize()方法

    上述8条原则无需手动添加任何同步手段(synchronized|volatile)即可达到效果,下面我们结合前面的案例演示这8条原则如何判断线程是否安全,如下:

    class MixedOrder{
        int a = 0;
        boolean flag = false;
        public void writer(){
            a = 1;
            flag = true;
        }
    
        public void read(){
            if(flag){
                int i = a + 1;
            }
        }
    }

    同样的道理,存在两条线程A和B,线程A调用实例对象的writer()方法,而线程B调用实例对象的read()方法,线程A先启动而线程B后启动,那么线程B读取到的i值是多少呢?现在依据8条原则,由于存在两条线程同时调用,因此程序次序原则不合适。writer()方法和read()方法都没有使用同步手段,锁规则也不合适。没有使用volatile关键字,volatile变量原则不适应。线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性和本次测试案例也不合适。线程A和线程B的启动时间虽然有先后,但线程B执行结果却是不确定,也是说上述代码没有适合8条原则中的任意一条,也没有使用任何同步手段,所以上述的操作是线程不安全的,因此线程B读取的值自然也是不确定的。修复这个问题的方式很简单,要么给writer()方法和read()方法添加同步手段,如synchronized或者给变量flag添加volatile关键字,确保线程A修改的值对线程B总是可见。

    volatile内存语义

    volatile在并发编程中很常见,但也容易被滥用,现在我们就进一步分析volatile关键字的语义。volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用

    • 保证被volatile修饰的共享gong’x变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。

    • 禁止指令重排序优化。

    volatile的可见性

    关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中,但是对于volatile变量运算操作在多线程环境并不保证安全性,如下

    public class VolatileVisibility {
        public static volatile int i =0;
    
        public static void increase(){
            i++;
        }
    }

    正如上述代码所示,i变量的任何改变都会立马反应到其他线程中,但是如此存在多条线程同时调用increase()方法的话,就会出现线程安全问题,毕竟i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。

    public class VolatileVisibility {
        public static int i =0;
    
        public synchronized static void increase(){
            i++;
        }
    }

    现在来看另外一种场景,可以使用volatile修饰变量达到线程安全的目的,如下

    public class VolatileSafe {
    
        volatile boolean close;
    
        public void close(){
            close=true;
        }
    
        public void doWork(){
            while (!close){
                System.out.println("safe....");
            }
        }
    }

    由于对于boolean变量close值的修改属于原子性操作,因此可以通过使用volatile修饰变量close,使用该变量对其他线程立即可见,从而达到线程安全的目的。那么JMM是如何实现让volatile变量对其他线程立即可见的呢?实际上,当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量。volatile变量正是通过这种写-读方式实现对其他线程可见(但其内存语义实现则是通过内存屏障,稍后会说明)。

    volatile禁止重排优化

    volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。
    内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。下面看一个非常典型的禁止重排优化的例子DCL,如下:

    /**
     * Created by zejian on 2017/6/11.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public class DoubleCheckLock {
    
        private static DoubleCheckLock instance;
    
        private DoubleCheckLock(){}
    
        public static DoubleCheckLock getInstance(){
    
            //第一次检测
            if (instance==null){
                //同步
                synchronized (DoubleCheckLock.class){
                    if (instance == null){
                        //多线程环境下可能会出现问题的地方
                        instance = new DoubleCheckLock();
                    }
                }
            }
            return instance;
        }
    }

    上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

    memory = allocate(); //1.分配对象内存空间
    instance(memory);    //2.初始化对象
    instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null

    由于步骤1和步骤2间可能会重排序,如下:

    memory = allocate(); //1.分配对象内存空间
    instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
    instance(memory);    //2.初始化对象

    由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

      //禁止指令重排优化
      private volatile static DoubleCheckLock instance;

    ok~,到此相信我们对Java内存模型和volatile应该都有了比较全面的认识,总而言之,我们应该清楚知道,JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性。

    如有错误,欢迎留言,谢谢!

    参考资料:
    http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
    http://blog.csdn.net/iter_zc/article/details/41843595
    http://ifeve.com/wp-content/uploads/2014/03/JSR133%E4%B8%AD%E6%96%87%E7%89%881.pdf

    《深入理解JVM虚拟机》
    《Java高并发程序设计》

    展开全文
  • JVM内存模型

    2018-12-02 15:38:29
    JVM 内存模型整理
  • java内存模型

    千次阅读 多人点赞 2018-11-09 13:09:55
    java内存模型 下图就是java内存模型,但是一般讨论的时候不会画这个图,一般画的是java内存模型抽象结构图(在下文)。Thread Stack就是java内存模型抽象结构图中的本地内存,Heap就是java内存模型抽象结构图中的主...

    java内存模型

    下图就是java内存模型,但是一般讨论的时候不会画这个图,一般画的是java内存模型抽象结构图(在下文)。Thread Stack就是java内存模型抽象结构图中的本地内存,Heap就是java内存模型抽象结构图中的主内存。接下来介绍下图中两个线程内存分配的概念。

    java里的堆是运行时的数据区,堆是由垃圾回收来负责的,堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。java的垃圾收集器会自动收走不再使用的数据。缺点是,由于在运行时动态分配内存,因此存取速度相对慢一些。

    栈的优势是存取速度比heap要快,仅次于计算机里的寄存器。Stack的数据是可以共享的。但是它的缺点是Stack中的数据大小和生存期必须是确定的。缺乏灵活性。Stack中主要存放基本类型的变量。比如int,short,long,float,double,byte,char等。

    java内存模型要求调用栈和本地变量存放在线程栈上。对象存放在堆上。一个本地变量它也可能是指向对象的引用,引用的这个本地变量是存在线程栈上,但是对象本身是存放在堆上,比如图中的Local variable 1 是本地变量存放在线程栈上,Object 1 是对象存放在堆上。一个对象可能包含方法图中methodOne()和methodTwo(),可能包含本地变量Local variable 1和Local variable 2,这些本地变量是存放在线程栈上,即使这些方法所属的对象存放在堆上,一个对象的成员变量可能会随着这个对象自身存放在堆上,不管这个成员变量是基本类型还是引用类型。静态成员变量跟着类的定义一起存放在堆上,存放在堆上的对象可以被所持有对这个对象引用的线程访问,比如图中Thread Stack中存放了对Object 3 的引用,那么是可以访问Object3的。如果两个线程同时调用同一个对象的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。

    计算机架构的简单图示

    首先介绍cpu,现在计算机通常用两个或多个cpu,其中一些cpu还有多核,从这一点我们可以看出,在一个有两个或者多个cpu的计算机上,同时运行多个线程是非常有可能的,而且每个cpu在某一个时刻,运行一个线程是肯定没有问题的。这意味着如果java程序是多线程的,在java程序中每个cpu上一个线程是可能同时并发执行的。

    介绍完cpu,然后是cpu寄存器(CPU Registers),每个cpu都包含一系列的寄存器,它们是cpu内存的基础,cpu在寄存器上执行操作的速度远大于在主存上执行的速度,这是因为cpu访问寄存器的速度远大于主存。

    高速缓存Cache,由于计算机的存储设备与处理器的运算速度之间有几个数量级的差距。所以现在计算机系统都不得不加入一层读写速度都尽可能接近处理器运算速度的高级缓存来作为内存与处理器之间的缓冲,将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后,在从缓存同步到内存中。这样处理器就无需等待缓慢的内存读写。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还是要慢一点。每个CPU可能有一个CPU的缓存层,一个CPU还有多层缓存。在某一时刻一个或多个缓存行可能被读到缓存,一个或多个缓存行可能被刷新回主存。同一时间点可能有很多操作在里面。

    CPU内存,所有的cpu都可以访问主存,主存通常比cpu的缓存大的多。

    运作原理

    通常情况下,当一个cpu需要读取主存的时候它会将主存的部分读取到cpu缓存中。它甚至会将缓存的部分内容读到内部寄存器里,然后在寄存器中执行操作,当cpu需要将结果回写到主存的时候,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

    通过图可以看出java内存模型与硬件架构之间存在一些差异,硬件内存架构它没有区分线程栈和堆,对于硬件而言所有的线程栈和堆都分布在主内存里,部分cpu栈和堆可能出现cpu缓存中和cpu内部的寄存器里面。

    java内存模型抽象结构图

    为了屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果,Java虚拟机规范中定义了Java内存模型。

    java内存模型(Java Memory Model, JMM)是一种规范,它规范了java虚拟机与计算机内存是如何协同工作的。它规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值。以及在必须时如何同步的访问共享变量 。

    java内存模型一般指的就是下图。

    线程之间的共享变量存储在主内存里面,每个线程都有一个私有的本地内存,本地内存是java内存模型的抽象概念,它并不是真实存在的。它涵盖了缓存,写缓存区,寄存器以及其他硬件和编译器的优化。本地内存存储了该线程以读或写共享变量拷贝的副本。比如线程A要是用共享变量的副本它首先要拷贝到本地内存A。从更低的层次来说主内存就是硬件内存,是为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。

    Java内存模型中的线程的工作内存是cpu的寄存器和高速缓存的抽象描述。而JVM静态存储模型就是jvm内存模型它只是对内存的物理划分而已,它只局限于内存,而且只局限于jvm内存。线程之间通信必须要通过主内存(主内存其实是堆内存)。

    java内存模型-同步的八种操作

    lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。

    unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

    read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。

    load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

    use(使用):作用于工作内存的变量,把工作内存中的一个变量值转递给执行引擎。

    assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量。

    store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

    write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

    java内存模型-同步规则

    1、如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

    2、不允许read和load、store和write操作之一单独出现

    3、不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。

    4、不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。

    5、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

    6、一个变量在同一时刻只允许一条线程对其进行locd操作,但lock操作可以被同一条线程重复执行多次,多次执行load后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。

    7、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行Load或assign操作初始化变量的值。

    8、如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

    9、对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

    java内存模型-同步操作与规则图示

     

    展开全文
  • 深入理解 Java 内存模型,由程晓明编著,深入理解java内存模型JMM
  • Go 内存模型 - Go 编程语言。
  • 深入Java 内存模型

    2018-08-15 14:57:57
    深入Java 内存模型本书介绍了,如何深入学习了解JAVA 内存模型!更好的了解java 虚拟机!
  • java 锁 内存模型, 对于想了解cpu锁,内存模型的同学是很不错的资料
  • jvm内存模型和java内存模型

    千次阅读 多人点赞 2018-12-03 21:46:29
    初识java虚拟机,就碰到一系列不懂的问题。我们以前常说java把局部变量放在栈里,new出来的变量放在堆里,然后堆里的数据不定时就给回收了。...直至今天,我看了java线程通讯之java内存模型,出现了主内存、...

    初识java虚拟机,就碰到一系列不懂的问题。我们以前常说java把局部变量放在栈里,new出来的变量放在堆里,然后堆里的数据不定时就给回收了。然后,如果是多线程的话,每个线程自己都有会一个私有的虚拟机栈,运行每个方法时都会创建一个栈帧,栈帧里存储着局部变量啦等信息,方法结束栈帧就出栈。这好像就是我们对java内存模型的第一次理解吧。直至今天,我看了java线程通讯之java内存模型,出现了主内存、工作内存等相关的字眼,说什么工作内存从主内存拷贝一份共享变量做高速缓存,每个线程拥有一个工作内存,这什么内存模型,又解决了什么问题呢。甚是不解,搜索了半天。原来一开始我们就搞混乱了。我们之前说的栈堆那个是JVM内存模型啊。

    区别:JVM内存模型则是指JVM的内存分区,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。

    我们可以看到,Java堆和方法区的区域是多个线程共享的数据区域。也就是说,多个线程可以操作保存在堆或者方法区中的同一个数据。这也就是我们常说的“Java的线程间通过共享内存进行通信”。那么java虚拟机从底层就怎么操作的呢。

    JMM并不像JVM内存结构一样是真实存在的,只是一个抽象的概念。JMM是和多线程相关的,描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。简单总结下,Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatile、synchronized等关键字。所以,我理解的就是JMM就是为了解决Java多线程对共享数据的读写一致性问题而产生的一种模型!

     

    下图是反映了主内存与线程工作内存(即是本地内存)之间的关系:

    JMM的主要目标是实现在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不同步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,因为后者是线程私有的,不会共享,当然不存在数据竞争问题。所以,我理解的是上图中的共享变量只是堆中的部分变量。而JMM规定了所有的变量都存储在主内存中,这个变量我想都是堆变量,如果硬是要跟JVM做一下匹配,那么我想主内存就相当于堆内存和方法区。然后线程copy的共享变量只是对部分堆变量的copy。那么,可想而知,如果硬是要匹配,我想工作内存里是包含两部分:线程私有的栈(这里不共享,也就没啥事),对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓冲区)。

    看张图就清晰了,

    每个线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。

    分析上图:

    1.线程A把从工作内存更新过的共享变量刷新到主内存中去。

    2.线程B到主内存中去读取线程A刷新过的共享变量,然后copy一份到自己的工作内存里去。

    虽然是这样,并没有解决并发问题啊,不可见?不具备原子性。这时候JMM定义了一些语法集,就是volatile、synchronized啦。

    例如可见性吧,一个线程对共享变量做了修改之后,其他的线程立即能够看到该变量这种变化。Java内存模型是对共享变量进行volatile修饰,然后一个线程通过将在工作内存中的变量修改后的值同步到主内存,另一个线程会立马会看到变化,然后从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的。

     

    总之,JVM内存模型是真的内存结构管理,Java内存模型只是为了适应和解决多线程通信而产生的一种模型,通过一些关键字修饰就可以实现并发。

     

    展开全文
  • JVM内存结构和Java内存模型别再傻傻分不清了

    万次阅读 多人点赞 2020-03-04 20:53:30
    JVM内存结构和Java内存模型都是面试的热点问题,名字看感觉都差不多,网上有些博客也都把这两个概念混着用,实际上他们之间差别还是挺大的。 通俗点说,JVM内存结构是与JVM的内部存储结构相关,而Java内存模型是与多...

    JVM内存结构和Java内存模型都是面试的热点问题,名字看感觉都差不多,网上有些博客也都把这两个概念混着用,实际上他们之间差别还是挺大的。
    通俗点说,JVM内存结构是与JVM的内部存储结构相关,而Java内存模型是与多线程编程相关,本文针对这两个总是被混用的概念展开讲解。

    JVM内存结构

    JVM构成

    说到JVM内存结构,就不会只是说内存结构的5个分区,而是会延展到整个JVM相关的问题,所以先了解下JVM的构成。

    在这里插入图片描述

    • Java源代码编译成Java Class文件后通过类加载器ClassLoader加载到JVM中
      • 类存放在方法区
      • 类创建的对象存放在
      • 堆中对象的调用方法时会使用到虚拟机栈,本地方法栈,程序计数器
      • 方法执行时每行代码由解释器逐行执行
      • 热点代码由JIT编译器即时编译
      • 垃圾回收机制回收堆中资源
      • 和操作系统打交道需要调用本地方法接口

    JVM内存结构

    程序计数器

    在这里插入图片描述
    (通过移位寄存器实现)

    • 程序计数器是线程私有的,每个线程单独持有一个程序计数器
    • 程序计数器不会内存溢出

    虚拟机栈

    • 栈:线程运行需要的内存空间

    • 栈帧:每一个方法运行需要的内存(包括参数,局部变量,返回地址等信息)

    • 每个线程只有一 个活动栈帧(栈顶的栈帧),对应着正在执行的代码
      在这里插入图片描述

    • 常见问题解析

      • 垃圾回收是否涉及栈内存:不涉及,垃圾回收只涉及堆内存

      • 栈内存分配越大越好吗:内存一定时,栈内存越大,线程数就越少,所以不应该过大

      • 方法内的局部变量是否是线程安全的:

        • 普通局部变量是安全的
        • 静态的局部变量是不安全的
        • 对象类型的局部变量被返回了是不安全的
        • 基本数据类型局部变量被返回时安全的
        • 参数传入对象类型变量是不安全的
        • 参数传入基本数据类型变量时安全的
      • 栈内存溢出(StackOverflowError)

        • 栈帧过多

          • 如递归调用没有正确设置结束条件
        • 栈帧过大

          • json数据转换 对象嵌套对象 (用户类有部门类属性,部门类由用户类属性)
        • 线程运行诊断

          • CPU占用过高(定位问题)

            • ‘top’命令获取进程编号,查找占用高的进程
            • ‘ps H -eo pid,tid,%cpu | grep 进程号’ 命令获取线程的进程id,线程id,cpu占用
            • 将查看到的占用高的线程的线程号转化成16进制的数 :如6626->19E2
            • ‘ jstack 进程id ’获取进程栈信息, 查找‘nid=0X19E2’的线程
            • 问题线程的最开始‘#数字’表示出现问题的行数,回到代码查看
          • 程序运行很长时间没有结果(死锁问题)

            • ‘ jstack 进程id ’获取进程栈信息
            • 查看最后20行左右有无‘Fount one Java-level deadlock’
            • 查看下面的死锁的详细信息描述和问题定位
            • 回到代码中定位代码进行解决

    本地方法栈

    • 本地方法栈为虚拟机使用到的 Native 方法服务
    • Native 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口
    • 如notify,hashcode,wait等都是native方法

    • 通过new关键字创建的对象都会使用堆内存

    • 堆是线程共享的

    • 堆中有垃圾回收机制

    • 堆内存溢出(OutOfMemoryError)

      • 死循环创建对象
    • 堆内存诊断

      • 命令行方式

        • ‘jps’获取运行进程号
        • ‘jmap -heap 进程号’查看当前时刻的堆内存信息
      • jconsole

        • 命令行输入jconsole打开可视化的界面连接上进程
        • 可视化的检测连续的堆内存信息
      • jvisualvm

        • 命令行输入jvisualvm打开可视化界面选择进程
        • 可视化的查看堆内存信息

    方法区

    • 方法区只是一种概念上的规范,具体的实现各种虚拟机和不同版本不相同
      • HotSpot1.6 使用永久代作为方法区的实现
      • HotSpot1.8使用本地内存的元空间作为方法区的实现(但StringTable还是放在堆中)
        在这里插入图片描述
    • 常见问题
      • StringTable特性

        • 常量池中的字符串仅是字符,第一次使用时才变为对象

        • 利用串池机制,避免重复创建字符串

        • 字符串常量拼接原理是StringBuilder(1.8)

        • 字符串常量拼接原理是编译器优化

        • StringTable在1.6中存放在永久代,在1.8中存放在堆空间

        • intern方法主动将串池中没有的字符串对象放入串池

          • 1.8中:尝试放入串池,如果有就不放入,只返回一个引用;如果没有就放入串池,同时返回常量池中对象引用

          • 1.6中:尝试放入串池,如果有就不放入,只返回一个引用;如果没有就复制一个放进去(本身不放入),同时返回常量池中的对象引用

          • 字符串常量池分析(1.8环境)

            String s1 = "a";
            String s2 = "b";
            String s3 = "a"+"b";
            String s4 = s1+s2;
            String s5 = "ab";
            String s6 = s4.intern();
            
            
            System.out.println(s3==s4);// s3在常量池中,s4在堆上(intern尝试s4放入常量池,因为ab存在了就拒绝放入返回ab引用给s6,s4还是堆上的)
            System.out.println(s3==s5);// s3在常量池中,s4也在常量池中(字符串编译期优化)
            System.out.println(s3==s6);// s3在常量池中,s6是s4的intern返回常量池中ab的引用,所以也在常量池中
            
            
            String x2 = new String("c")+new String("d");
            String x1 = "cd";
            x2.intern();
            
            System.out.println(x1==x2);//x2调用intern尝试放入常量池,但常量池中已经有cd了,所以只是返回一个cd的引用,而x2还是堆上的引用
            
      • JVM调优三大参数(如: java -Xms128m -Xmx128m -Xss256k -jar xxxx.jar)

        • -Xss:规定了每个线程虚拟机栈的大小(影响并发线程数大小)
        • -Xms:堆大小的初始值(超过初始值会扩容到最大值)
        • -Xmx:堆大小的最大值(通常初始值和最大值一样,因为扩容会导致内存抖动,影响程序运行稳定性)
      • JVM内存结构中堆和栈的区别

        • 管理方式:栈自动释放,堆需要GC
        • 空间大小:栈比堆小
        • 碎片:栈产生的碎片远少于堆
        • 分配方式:栈支持静态分配和动态分配,堆只支持动态分配
        • 效率:栈的效率比堆高

    GC垃圾回收机制

    1. 垃圾判别方法

    引用计数算法
    • 判断对象的引用数量来决定对象是否可以被回收

    • 每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1

    • 优点:执行效率高,程序执行受影响小

    • 缺点:无法检测出循环引用的情况,导致内存泄露

    可达性分析算法
    • Java虚拟机中的垃圾回收器采用可达性分析来探索所有存活对象

    • 扫描堆中的对象,看是否能沿着GC Root对象为起点的引用链找到该对象,找不到则可以回收

    • 哪些对象可以作为GC Root

    • 通过System Class Loader或者Boot Class Loader加载的class对象,通过自定义类加载器加载的class不一定是GC Root

      • 虚拟机栈中的引用的对象

      • 本地方法栈中JNI(natice方法)的引用的对象

      • 方法区中的常量引用的对象

      • 方法区中的类静态属性引用的对象

      • 处于激活状态的线程

      • 正在被用于同步的各种锁对象

      • GC保留的对象,比如系统类加载器等。

    2. 垃圾回收算法

    标记清除法
    • 标记没有被GC Root引用的对象
    • 清除被标记位置的内存
    • 优点:处理速度快
    • 缺点:造成空间不连续,产生内存碎片
      在这里插入图片描述
    标记整理法
    • 标记没有被GC Root引用的对象
    • 整理被引用的对象
    • 优点:空间连续,没有内存碎片
    • 缺点:整理导致效率较低

    在这里插入图片描述

    复制算法
    • 分配同等大小的内存空间
    • 标记被GC Root引用的对象
    • 将引用的对象连续的复制到新的内存空间
    • 清除原来的内存空间
    • 交换FROM空间和TO空间
    • 优点:空间连续,没有内存碎片
    • 缺点:占用双倍的内存空间
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    3. 分代垃圾回收机制

    • 分代垃圾回收流程
      在这里插入图片描述

      • 对象首先分配在伊甸园区域
      • 新生代空间不足时,触发Minor GC,伊甸园和from存活的对象使用【复制算法】复制到to中,存活的对象年龄加一,并且交换from区和to区
      • Minor GC会引发Stop the world(STW)现象,暂停其他用户的线程。垃圾回收结束后,用户线程才恢复运行
      • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4位二进制)
      • 当老年代空间不足,会先尝试触发Minor GC,如果之后空间仍不足,会触发Full GC(STW时间更长,老年代可能使用标签清除或标记整理算法)
      • 当存放大对象新生代放不下而老年代可以放下,大文件会直接晋升到老年代
      • 当存放大对象新生代和老年代都放不下时,抛出OOM异常
    • 默认堆内存分配
      在这里插入图片描述

      • 新生代占1/3,老年代占2/3
      • -XX:NewRatio:老年代和年轻代内存大小的比例
      • 新生代中按8 1 1进行分配,两个幸存区大小需要保持一致
      • -XX:SurvivorRatio: Eden和Survivor的比值,默认是8(8:1)
    • GC相关VM参数
      在这里插入图片描述

    4. 垃圾回收器

    • 安全点(SafePoint)

      • 分析过程中对象引用关系不会发生改变的点

      • 产生安全点的地方:

        • 方法调用
        • 循环跳转
        • 异常跳转
      • 安全点的数量应该设置适中

    • 串行(SerialGC)

      • 单线程的垃圾回收器
      • 堆内存较小,CPU核数少,适合个人电脑
      • SerialGC收集器 (-XX:+UseSerialGC 复制算法) Client模式下默认的年轻代收集器
      • SerialGC Old收集器 (-XX:+UseSerialOldGC 标记-整理算法)Client模式下默认的老年代收集器
        在这里插入图片描述
    • 吞吐量优先(ParallelGC)

      • 多线程的垃圾回收器
      • 堆内存较大,多核CPU,适合服务器
      • 尽可能让单位时间内STW暂停时间最短(吞吐量=运行代码时间/(运行代码时间+垃圾回收时间))
      • 并行的执行
      • ParallelGC收集器(-XX:+UseParallelGC 复制算法) Server模式下默认的年轻代垃圾回收器
      • ParallelGC Old收集器(-XX:+UseParallelOldGC 复制算法)

      在这里插入图片描述

    • 响应时间优先(CMS -XX:+UseConcMarkSweepGC 标记清除算法)

      • 多线程的垃圾回收器

      • 堆内存较大,多核CPU,Server模式下默认的老年代垃圾回收器

      • 尽可能让单次STW暂停时间最短

      • 部分时期内可以并发执行

      • 执行流程

        • 初始标记:stop-the-world
        • 并发标记:并发追溯标记,程序不会停顿
        • 并发预清理:查找执行并发标记阶段从年轻代晋升到老年代的对象
        • 重新标记:暂停虚拟机,扫描CMS堆中的剩余对象
        • 并发清理:清理垃圾对象,程序不会停顿
        • 并发重置:重置CMS收集器的数据结构

    在这里插入图片描述

    • G1(-XX:+UseG1GC 复制+标记清除算法)

      • G1l垃圾回收器简介
      • 定义:Garbage First (2017 jdk9 默认)
      • 特点
        • 并发和并行
        • 分代收集
        • 空间整合
        • 可预测的停顿
      • 使用场景
        • 同时注重吞吐量和低延迟,默认暂停目标是200ms
        • 超大堆内存,会将整个堆划分为多个大小相等的Region(新生代和老年代不再物理隔离了)
        • 整体上是标记整理算法,两个区域之间是复制算法
    • 垃圾回收阶段

      • 新生代垃圾收集

        • 会发生STW
      • 新生代垃圾收集+并发标记

        • 在Young GC时会进行GC Root的初始标记
        • 老年代占用堆内存空间比例达到阈值时,进行并发标记(不会STW)
      • 混合收集,对新生代,幸存区和老年代都进行收集

        • 最终标记,会STW
        • 拷贝存活,会STW
        • 三种阶段循环交替
          在这里插入图片描述
    • Full GC

      • SerialGC

        • 新生代内存不足发生的垃圾收集:minor GC
        • 老年代内存不足发生的垃圾收集:full GC
      • ParallelGC

        • 新生代内存不足发生的垃圾收集:minor GC
        • 老年代内存不足发生的垃圾收集:full GC
      • CMS

        • 新生代内存不足发生的垃圾收集:minor GC

        • 老年代内存不足

          • 并发收集成功:并发的垃圾收集
          • 并发收集失败:串行的full GC
      • G1

        • 新生代内存不足发生的垃圾收集:minor GC

        • 老年代内存不足,达到阈值时进入并发标记和混合收集阶段

          • 如果回收速度>新产生垃圾的速度 :并发垃圾收集
          • 如果回收速度<新产生垃圾的速度:串行的full GC

    5. 四种引用

    在这里插入图片描述

    • 强引用

      • 最常见的对象:通过new关键字创建,通过GC Root能找到的对象。
      • 当所有的GC Root都不通过【强引用】引用该对象时,对象才能被垃圾回收
    • 软引用

      • 仅有【软引用】引用该对象时,在垃圾回收后,内存仍不足时会再次发起垃圾回收,回收软引用对象

      • 可以配合引用队列来释放软引用自身

      • 创建一个软引用:SoftReference ref = new SoftReference<>(new Object());

      • 软引用被回收后,仍然还保留一个null,如将软引用加入集合,回收后遍历集合仍然还存在一个null

        • 解决:使用引用队列,软引用关联的对象被回收时,软引用自身会被加入到引用队列中,通过queue.poll()取得对象进行删除
        • 创建一个而引用队列:ReferenceQueue queue = new ReferenceQueue<>();
        • 创建加入了引用队列的软引用:SoftReference ref = new SoftReference<>(new Object(),queue);
    • 弱引用

      • 仅有【弱引用】引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
      • 可以配合引用队列来释放弱引用自身
      • 创建一个弱引用:WeakReference ref = new WeakReference<>(new Object());
      • 引用队列使用同软引用
    • 虚引用

      • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将【虚引用】入队,由Reference Hanler线程调用虚引用相关方法释放【直接内存】(unsafe类中方法)
    • 终结器引用

      • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用队列入队(引用对象暂未回收),再由Finalizer线程通过终结器引用找到被引用对象并调用他的finalize方法,第二次gc时回收被引用对象

    类加载

    类加载器的分类

    在这里插入图片描述

    类加载过程

    在这里插入图片描述

    • 加载

      • 通过ClassLoader加载Class文件字节码,生成Class对象
    • 链接

      • 校验:检查加载的的Class的正确性和安全性

      • 准备:为类变量分配存储空间并设置类变量初始值

      • 解析:JVM将常量池内的符号引用转换为直接引用

    • 初始化

      • 执行类变量赋值和静态代码块

    LoadClass和forName的区别

    • Class.ForName得到的class是已经初始化完成的
    • ClassLoader.loadClass得到的class是还没有链接的

    双亲委派机制

    在这里插入图片描述

    • 什么是双亲委派机制
      • 当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
    • 为什么要使用双亲委派机制
      • 防止重复加载同一个.class文件,通过委托去向上级问,加载过了就不用加载了。
      • 保证核心.class文件不会被串改,即使篡改也不会加载,即使加载也不会是同一个对象,因为不同加载器加载同一个.class文件也不是同一个class对象,从而保证了class执行安全

    自定义类加载器

    • 需求场景

      • 想要加载非classpath的随意路径的类文件
      • 通过接口来使用实现,希望解耦合
    • 步骤

      • 继承Classloader父类
      • 遵循双亲委派机制,重写findClass方法(不能重写loadClass,重写了就不符合双亲委派了)
      • 读取类的字节码
      • 调用父类的defineClass方法加载类
      • 使用者调用类加载的loadClass方法
    • 案例演示

    创建自定义类加载器

    public class MyClassLoader extends ClassLoader {
        private String path;
        private String classLoaderName;
    
        public MyClassLoader(String path, String classLoaderName) {
            this.path = path;
            this.classLoaderName = classLoaderName;
        }
    
        //用于寻找类文件
        @Override
        public Class findClass(String name) {
            byte[] b = loadClassData(name);
            return defineClass(name, b, 0, b.length);
        }
    
    
        //用于加载类文件
        private byte[] loadClassData(String name) {
            name = path + name + ".class";
    
            try (InputStream in = new FileInputStream(new File(name));
                 ByteArrayOutputStream out = new ByteArrayOutputStream();) {
                int i = 0;
                while ((i = in.read()) != -1) {
                    out.write(i);
                }
                return out.toByteArray();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    调用自定义类加载器加载类

    public class MyClassLoaderChecker {
        public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
            MyClassLoader m = new MyClassLoader("C:\\Users\\73787\\Desktop\\","myClassLoader");
            Class<?> c = m.loadClass("Robot");
            System.out.println(c.getClassLoader());
            c.newInstance();
        }
    }
    

    反射机制

    反射的定义

    JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

    反射的常用场景

    第三方应用开发过程中,会需要某个类的某个成员变量、方法或是属性是私有的或者只对系统应用开放,就可以通过Java的反射机制来获取所需的私有成员或者方法

    反射相关的类

    在这里插入图片描述

    Class类:

    代表类的实体,在运行的Java应用程序中表示类和接口

    • 获得类的方法

    在这里插入图片描述

    • 获得类中属性的方法

    在这里插入图片描述

    • 获得类中方法的方法
      在这里插入图片描述
    • 获取类中构造器的方法
      在这里插入图片描述
    Filed类

    Filed代表类的成员变量(属性)

    在这里插入图片描述

    Method类

    在这里插入图片描述

    Constructor类

    在这里插入图片描述

    案例

    定义一个Robot类

    public class Robot {
        //私有属性
        private String name;
        //公有方法
        public void sayHi(String hello){
            System.out.println(hello+" "+name);
        }
        //私有方法
        private String thorwHello(String tag){
            return "hello "+tag;
        }
    }
    

    编写一个反射应用类,针对私有的属性和方法必须设置setAccessible(true)才能进行访问

    public class ReflectSample {
        public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
            //加载类
            Class<?> rc = Class.forName("leetcode.Robot");
            //获取类实例
            Robot r = (Robot)rc.newInstance();
            //打印类名
            System.out.println(rc.getName());
            
            //加载一个私有方法
            Method getHello = rc.getDeclaredMethod("thorwHello",String.class);
            getHello.setAccessible(true);
            Object bob = getHello.invoke(r, "bob");
            System.out.println(bob);
           
             //加载一个公有方法
            Method sayHi = rc.getMethod("sayHi",String.class);
            Object welcome = sayHi.invoke(r,"welcome");
           
             //加载一个私有属性
            Field name = rc.getDeclaredField("name");
            name.setAccessible(true);
            name.set(r,"tom");
            sayHi.invoke(r,"welcome");
        }
    }
    

    Java内存模型

    什么是Java内存模型(JMM)

    • 通俗来说,JMM是一套多线程读写共享数据时,对数据的可见性,有序性和原子性的规则

    为什么会有Java内存模型

    JVM实现不同会造成“翻译”的效果不同,不同CPU平台的机器指令有千差万别,无法保证同一份代码并发下的效果一致。所以需要一套统一的规范来约束JVM的翻译过程,保证并发效果一致性

    原子性

    • 什么是原子性
      • 原子性指一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分的。
    • 原子性怎么实现
      • 使用synchronized或Lock加锁实现,保证任一时刻只有一个线程访问该代码块
      • 使用原子操作
    • Java中的原子操作有哪些
      • 除long和double之外的基本类型的赋值操作(64位值,当成两次32位的进行操作)
      • 所有引用reference的赋值操作
      • java.concurrent.Atomic.*包中所有类的原子操作
    • 创建对象的过程是否是原子操作(常应用于双重检查+volatile创建单例场景)
      • 创建对象实际上有3个步骤,并不是原子性的
        • 创建一个空对象
        • 调用构造方法
        • 创建好的实例赋值给引用

    可见性

    • 什么是可见性问题
      • 可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。
    • 为什么会有可见性问题、
      • 对于单线程程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
      • 对于多线程程序而言。由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
        在这里插入图片描述
    • 如何解决可见性问题
      • 解决方法1:加volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值
      • 解决方法2:使用synchronized和Lock保证可见性。因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中
    • 案例
    /**
    * 〈可见性问题分析〉
    *
    * @author Chkl
    * @create 2020/3/4
    * @since 1.0.0
    */
    public class FieldVisibility {
        int a = 1;
        int b = 2;
    
        private void change() {
            a = 3;
            b = a;
        }
        private void print() {
            System.out.println("b=" + b + ";a=" + a);
        }
        public static void main(String[] args) {
            while (true) {
                FieldVisibility test = new FieldVisibility();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        test.change();
                    }
                }).start();
    
    
    
    
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        test.print();
                    }
                }).start();
            }
        }
    }
    

    循环创建两类线程,一个线程用于做值的交换,一个线程用于打印值

    比较直观的三种结果

    • 打印线程先执行:b = 2, a = 1
    • 交换线程先执行:b = 3, a = 3
    • 交换线程执行到一半就切出去打印了,只执行了a=3赋值操作:b = 2 , a =3

    实际上除了很容易想到的三种情况外还有一种特殊情况:b = 3 , a = 1

    • 这种情况就是可见性问题
    • a的值在线程1(执行交换线程)的本地缓存中进行了更新,但是并没有同步到共享缓存,而b的值成功的更新到了共享缓存,导致线程2(执行打印线程)从共享缓存中获取到的数据并不是实时的最新数据
      -在这里插入图片描述

    有序性(重排序)

    • 什么是重排序
      • 在线程内部的两行代码的实际执行顺序和代码在Java文件中的逻辑顺序不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序。
    • 重排序的意义
      • JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
      • 案例
        计算:
        a = 3;
        b = 2;
        a = a + 1;
        重排序优化前的instructions
        
        load a
        set to 3
        store 3
        
        load b
        set to 2
        store b
        
        load a
        set to 4
        store a
        
        经过重排序处理后
        
        load a
        set to 3
        set to 4
        store a
        
        
        load b
        set to 2
        store b
        
        上述少了两个指令,优化了性能
        
    • 重排序的3种情况
      • 编译器优化( JVM,JIT编辑器等): 编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序
      • 指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
      • 内存系统的重排序: 由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
        在这里插入图片描述

    volatile

    • 什么是volatile

      • volatile是一种同步机制,比synchronized或者Lock相关类更轻量级,因为使用volacile并不会发生上下文切换等开销很大的行为
      • volatile是无锁的,并且只能修饰单个属性
    • 什么时候适合用vilatile

      • 一个共享变量始终只被各个线程赋值,没有其他操作
      • 作为刷新的触发器,引用刷新之后使修改内容对其他线程可见(如CopyOnRightArrayList底层动态数组通过volatile修饰,保证修改完成后通过引用变化触发volatile刷新,使其他线程可见)
    • volatile的作用

      • 可见性保障:修改一个volatile修饰变量之后,会立即将修改同步到主内存,使用一个volatile修饰的变量之前,会立即从主内存中刷新数据。保证读取的数据都是最新的,之前的修改都是可见的。
      • 有序性保障(禁止指令重排序优化):有volatile修饰的变量,赋值后多了一个“内存屏障“( 指令重排序时不能把后面的指令重排序到内存屏障之前的位置)
    • volatile的性能

      • volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

    happens-before规则

    什么是happens-before规则:前一个操作的结果可以被后续的操作获取。

    • 程序的顺序性规则:在一个线程内一段代码的执行结果是有序的。虽然还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
    • volatile规则: 就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
    • 传递性规则:happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
    • 管程锁定规则:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
    • 线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
    • 线程终止规则: 在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
    • 线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
    • 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

    如果有用,点个赞再走吧

    展开全文
  • Java内存模型

    2016-07-21 14:06:03
    Java内存模型
  • 内存模型就是对内存进行使用的一个规范。通过内存模型,我们能了解 C 语言是如何 对内存进行划分,如何使用每一部分内存的
  • Java多线程之Java内存模型

    千次阅读 多人点赞 2019-05-29 22:46:46
    在介绍Java内存模型之前,我们先介绍一下计算机硬件的内存模型,因为JVM的并发和物理机器的并发很相似,甚至JVM并发操作中很多设计都是因为计算机系统的设计引发的。 硬件的内存模型 大家都知道计算机系统处理任务...
  • 几种内存模型

    千次阅读 2018-12-19 08:19:40
    首先跟大家确定一个大前提是内存模型是与多线程息息有关的。这篇文章主要讨论的是多线程与内存的关系。 实际上,内存模型对单线程而言,它只保证程序在单线程执行的情况下,程序能够得到正确的结果。 一、内存模型是...
  • JAVA内存模型与JVM内存模型的区别

    千次阅读 2021-01-25 22:39:23
    JAVA内存模型: Java内存模型规定所有的变量都是存在主存中,每个线程都有自己的工作内存。线程堆变量的操作都必须在工作内存进行,不能直接堆主存进行操作,并且每个线程不能访问其他线程的工作内存。 Java内存...
  • 面试官:说说什么是 Java 内存模型(JMM)?

    万次阅读 多人点赞 2021-05-05 23:23:20
    1. 为什么要有内存模型? 1.1. 硬件内存架构 1.2. 缓存一致性问题 1.3. 处理器优化和指令重排序 2. 并发编程的问题 3. Java 内存模型 3.1. Java 运行时内存区域与硬件内存的关系 3.2. Java 线程与主内存的关系 ...
  • 详细介绍了JMM Java内存模型的概念、由来,以及happens-before原则的具体规则。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 685,529
精华内容 274,211
关键字:

内存模型