Java内存模型
- Java内存模型(Java Memory Model)简称JMM
- JMM与JVM的区别
- JMM都定义了那些规范
- 那JMM定义了什么,是围绕什么建立起来的
- JMM是如何去解决原子性&可见性&有序性问题的?
- 本文只是简单介绍了一下JMM的概念,下一篇,写一下volatile如何保证可见性。
Java内存模型(Java Memory Model)简称JMM
JMM与JVM的区别
很多人听到java的内存模型,感觉是不是JVM啊,其实他们是完全不同的概念,JVM是java的运行环境,而JMM是Java内存模型的缩写,它是Java程序中用来描述线程之间共享变量的规范。
下面先简单的描述JVM和JMM的区别。
JVM(Java Virtual Machine)
JVM是java的运行环境,本质上就是一个软件,负责将Java代码编译成字节码,并将字节码转化成机器码进行运行。JVM有很强的跨平台性,可以在不同的操作系统上运行Java文件,JVM是java的执行引擎,负责程序的内存管理、线程调度、垃圾回收等任务。
JMM(Java Memory Model)
JMM是Java内存模型的缩写,他是Java程序中用来描写线程之间共享变量的一种规范,JMM定义了Java程序中线程之间如何访问共享变量的规则,包括如何进行原子性操作、可见性、有序性等。JMM规定了一组规则和语义,确保在多线程情况下,对共享变量的访问是安全的和正确的。JMM的主要目标是提供一种规范,以确保Java程序在不同的硬件和操作系统上具有一致的行为。(就是为了屏蔽不同操作系统,硬件对Java线程共享变量的一种规范,来保证不同操作系统和硬件的正确使用)
因此,JVM和JMM的区别在于,JVM是Java的运行环境,负责程序的内存管理、线程调度、垃圾回收等任务。而JMM是Java程序中用来描述线程之间共享变量的规范,规定了在多线程环境下,对共享变量的访问的规则,保证共享变量访问正确性。
JMM都定义了那些规范
JMM定义了Java程序中各个变量(实例字段,静态变量和构成数组对象的元素)的访问方式
访问维度:规定所有变量都存储在主内存中,所有线程都可以访问,主内存是共享内存区域,每个线程的工作内存都是独立 , 线程 操作数据只能在工作内存中进行,然后刷回到主存。这是 Java 内存模型定义的线程基本工作方式。
读取赋值维度:规定线程线程对于变量的操作(读取赋值)必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作内存,然后对变量操作,操作完成之后再写回主内存, 不可以直接操作主内存的变量,工作内存是私有内存区域,每个线程都有自己的工作内存,不可以相互访问。
主内存
主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题。
工作内存
主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。
那JMM定义了什么,是围绕什么建立起来的
整个Java内存模型实际上是围绕着三个特征建立起来的。分别是:原子性,可见性,有序性。这三个特征是整个Java并发的基础。
原子性:
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
如:以下代码
X=10; //原子性(简单的读取、将数字赋值给变量)
Y = x; //变量之间的相互赋值,不是原子操作
X++; //对变量进行计算操作,不是院子操作,他这里其实是两步,第一将X+1,第二将新值X赋给X,多线程环境下就会出现安全问题
X = x+1;
JMM只能保证基本变量赋值的原子性,如果要保证一个代码块的原子性,提供了monitorenter 和 moniterexit 两个字节码指令,也就是 synchronized 关键字。因此在 synchronized 块之间的操作都是原子性的。
可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
但在多线程环境中,线程对于共享变量的操作都是拷贝到工作内存中进行的,在操作后才会写入主内存,所以在操作过程中,另一个线程拷贝主内存变量操作,并及时写入了主内存,这个时候两个线程是隔离的,就会造成可见性问题。另外指令重排以及编译器优化也可能导致可见性问题
有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。
JMM是如何去解决原子性&可见性&有序性问题的?
原子性问题
除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过 synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块,自然可以保证原子性。
可见性问题
volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。
有序性问题
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。