jmm java内存模型

jvm详解

1、CPU执行程序:

​ 从内存中取数据存到高速缓存中CPU从高速缓存中取数据,进行运算,运算完成将结果存到高速缓存中,高速缓存将数据刷新到内存中,CPU执行任务的最小单位是线程,四核八线程指的是CPU能并行执行八个线程,但是一般操作系统中线程的数量远远大于八个线程程所以执行的,所以操作系统中会有一定的调度算法来执行线程,如时间片轮转算法:将CPU的执行权分成时间片,操作系统的任务调度器负责分配时间片给线程,当一个线程的时间片被用完,它就会被切换出去,另一个线程或得CPU的执行权。

2、并行和并发

并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。

在这里插入图片描述

在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每
一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分
时交替运行的时间是非常短的。

3、进程和线程

进程:对正在运行程序的一种抽象,是资源分配和独立运行的基本单位。CPU执行任务的基本单位,每个进
程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系
统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。
一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

线程可以视为进程中的子任务,如迅雷可以同时下载多个文件,word能一边输入一边检查拼写

小结

​ 一个进程可以拥有多个线程。

​ 一个程序可以有多个进程(多次执行,也可以没有进程,不执行)

​ 一台机器上可以有多个JVM实例(也可以没有JVM实例)

​ 进程是指一段正在执行的程序

​ 线程是程序执行的最小单位。

通过多次执行一个程序可以有多个进程,通过调用一个进程可以有多个程序。

4、jvm和java程序

​ 程序运行时,会首先建立一个JVM实例----------所以说,JVM实例是多个的,每个运行的程序对应一个JVM实例。每个java程序都运行在一个单独的JVM实例上是一个进程,

JVM是一份本地化的程序,本质上是可执行的文件,是静态的概念。程序运行起来成为进程,是动态的概念。
java程序是跑在JVM上的,严格来讲,是跑在JVM实例上的,一个JVM实例其实就是JVM跑起来的进程,二者合起来称之为一个JAVA进程。

5、jvm虚拟机

虚拟机:是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机有自己完善的硬体架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。:

我们既然知道jvm实例就是一个进程,那它就会被分配内存资源,会将内存分为几个部分,在这里就引申出jvm内存模型

6、jvm内存模型

img

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方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程,如下(图有误,应该为栈桢):

    img

7、jmm

我们常说的JVM内存模式指的是JVM的内存分区;而JMM是一种虚拟机规范。

Java 内存模型(JMM)是一种抽象的概念,并不真实存在,它描述了一组规则或规范。

JMM主要目的是定义程序各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。

jmm 就是规定了在多线程情况下,如何对一个共享变量进行读写。

Java内存模型和硬件内存架构之间的桥接

Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内存中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示:

img

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:

  • 线程之间的共享变量存储在主内存(Main Memory)中

  • 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。

  • 线程中的栈是存储在主内存ram中,那么栈帧中的局部变量,返回值也都是存储在主内存中,它们是线程私有的我们可以直接操作它们,不用去关心一致性。不要去纠结jmm主要是关心共享变量的!!!!

    从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。

  • Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。

img

img

JMM模型下的线程间通信:

线程间通信必须要经过主内存。

如下,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤:

1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。

2)线程B到主内存中去读取线程A之前已更新过的共享变量。

img

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

JMM 三大特征

JMM 三大特征分别是:原子性,可见性,有序性。整个 JMM 实际上也是围绕着这三个特征建立起来的,并且也是 Java 并发编程的基础。

可见性

由上图我们知道,共享变量存储在主内存中,线程的工作内存存储共享变量的副本。

保证可见性就是:当一个线程对共享变量执行写的操作,该共享变量会立即刷新到主内存中且其他线程的工作内存中的共享变量的副本会立即失效,保证了其他线程对共享变量的可见性。

栗子一枚

package test.thread.volidate;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Test1 {
     static  boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (true){
               if (!flag){
                   System.out.println();
                   break;
               }
            }
        },"t1").start();
        Thread.sleep(1000);
        flag =false;
        log.debug("_____flag change______");
    }

}
//会执行失败,t1线程的循环不会被打断,主线程对flag的修改不会被t1线程感知。 
原子性

定义:在有关Java线程的讨论中,一个常不正确的知识是“原子操作不需要进行同歩控制”。原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生的“上下 文切换”之前(切换到其他线程执行)执行完毕。

《thinking in Java》的第21章的《并发》有写:“除了long和double类型,Java基本数据类型都是的简单读写都是原子的,而简单读写就是赋值和return语句。”因此而对于其他自加自减以及其他运算操作,是非原子操作。

但是,虽然读写基本类型是原子的,但其都是在工作内存层面的,由于线程有一个时间分片的概念,并不能保证该数据会被刷新到主内存,因此,声明为volative可以保证可视性。

同时,32位机器上更新double或long类型 不是单个操作,不加volatile的话 ,并发情况下针对某个元素的访问可能出现脏读(cpu cache导致的),单纯的替换如果允许脏读的话 ,可以不加这些修饰符 ,如果涉及到非幂等操作 ,还是要用同步修饰符。

package test.thread.basic;

public class Test5 {
    static int x;
    static int y;

    public static void main(String[] args) {
        x = 1;
        y = x;
    }
}
/**
 0 iconst_1
 1 putstatic #2 <test/thread/basic/Test5.x>  基本数据类型的赋值是原子操作。
   
 4 getstatic #2 <test/thread/basic/Test5.x>
 7 putstatic #3 <test/thread/basic/Test5.y>
 
10 return
*/

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:

int i = 0;              
boolean flag = false;
i = 1;                //语句1  
flag = true;          //语句2

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。

但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:

int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4

这段代码有4个语句,那么可能的一个执行顺序是:

在这里插入图片描述

那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

博客用于记录自己遇到的问题,转载了大量其他的文章写的比较乱。
侵删
推荐去看这个文章:https://my.oschina.net/laigous/blog/666542

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值