JMM

作者: 三十五岁养老 | 来源:发表于2022-03-04 14:30 被阅读0次

JVM定义了一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果

线程间通信

线程之间的通信机制有两种,共享内存消息传递
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。

线程同步

同步是指程序用于控制不同线程之间操作发生相对顺序的机制。
在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

JMM内存模型的实现

现代cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,为了避免由于处理器停顿下来等待向内存写入数据而产生的延迟,所以加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲

JVM对Java内存模型的实现

在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区
JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。


image.png
  • 所有原始类型(boolean,byte,short,char,int,long,float,double)的局部变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的局部变量,一个线程可以传递一个副本给另一个线程,当它们之间是无法共享的。
  • 堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的局部变量,它都会被存储在堆区。
  • 一个局部变量如果是原始类型,那么它会被完全存储到栈区。 一个局部变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
  • 对于一个对象的成员方法,这些方法中包含局部变量,仍需要存储在栈区,即使它们所属的对象在堆区。 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。

Java内存模型带来的问题

可见性问题

CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中:要解决共享对象可见性这个问题,我们可以使用java volatile关键字或者是加锁

竞争现象

线程A和线程B共享一个对象obj。假设线程A从主存读取Obj.count变量到自己的CPU缓存,同时,线程B也读取了Obj.count变量到它的CPU缓存,并且这两个线程都对Obj.count做了加1操作。此时,Obj.count加1操作被执行了两次,不过都在不同的CPU缓存中。如果这两个加1操作是串行执行的,那么Obj.count变量便会在原始值上加2,最终主存中的Obj.count的值会是3。然而下图中两个加1操作是并行的,不管是线程A还是线程B先flush计算结果到主存,最终主存中的Obj.count只会增加1次变成2,尽管一共有两次加1操作。 要解决上面的问题我们可以使用java synchronized代码块。

volatile的内存语义

volatile写的内存语义如下:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义如下:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

volatile变量自身具有下列特性:

  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

锁的内存语义

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

synchronized的实现原理

使用monitorenter和monitorexit指令实现的:

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
每个monitorenter必须有对应的monitorexit与之配对
任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态

synchronized和volatile的区别

1.volatile本质:是java虚拟机(JVM)当前变量在工作内存中的值是不确定的,需要从主内存中读取;synchronized则是锁定当前的变量,只有当前线程可以访问到该变量,其他的线程将会被阻塞。
2.volatile只能实现变量的修改可见性,并不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
3.volatile只能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

引申

原子性

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 switch (切换到另一个线程)
例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
在某些JVM中"a +=1"可能要经过这样三个步骤:
(1)取出a
(2)计算a+1
(3)将计算结果写入内存

多线程变量可见

被volatile修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。

如果给一个【变量加上volatile】修饰符,就相当于:每一个线程中一旦这个值发生了变化就马上刷新回主存,使得各个线程取出的值相同。编译器不要对这个变量的读、写操作做优化。但是值得注意的是,除了对long和double的简单操作之外,volatile并不能提供原子性。所以,就算你将一个变量修饰为volatile,但是对这个变量的操作并不是原子的,在并发环境下,还是不能避免错误的发生!

[参考资料]https://www.jianshu.com/p/8a58d8335270

相关文章

网友评论

    本文标题:JMM

    本文链接:https://www.haomeiwen.com/subject/cwkurrtx.html