JMM和内存问题

本文深入解析Java内存模型(JMM),阐述了工作内存、主内存的概念,以及多线程并发问题的根源,重点讲解了原子性、可见性、有序性、happens-before原则和volatile关键字的作用。理解这些概念有助于开发者编写高效、线程安全的Java程序。
摘要由CSDN通过智能技术生成

JMM

java内存模型,JMM 的主要目的是定义程序中各种变量的访问规则, 以实现让Java程序在各种平台下都能达到一致的内存访问效果。

  • JMM 规定所有实例变量和静态变量都存储在主内存,每条线程有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。不同线程间无法直接访问对方工作内存中的变量,线程通信必须经过主内存。

 

主内存:我们常说的物理内存,共享变量(实例变量,静态变量)都存储在主内存中 。

工作内存:指每个CPU提供的资源,包括cpu寄存器,cpu高速缓存。工作内存中保存了被该线程使用的共享变量的主内存副本,保存的是变量副本而不是对象本身。线程对变量的所有操作(读取、 赋值等) 都必须在工作内存中进行, 而不能直接读写主内存中的数据,不同的线程之间也无法直接访问对方工作内存中的变量。

主内存和工作内存与jvm内存模型没有任何联系,属于不同层面上的定义。如果一定要勉强对应起来,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。

内存之间的交互操作

一个变量如何从主内存拷贝到工作内存、 如何从工作内存同步回主内存这一类的实现细节, Java内存模型中定义了以下8种原子操作来完成。

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

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

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

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

use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

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

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

多线程之所以会导致并发问题,是因为:

1. 多核CPU,以及多核的cpu高速缓存,高速缓存是并发问题根本原因

2. 栈是线程私有的,而线程必须单独由一个cpu执行,也可以理解为cpu为当前栈私有的,所以这里会让人混淆了栈和工作内存。但事实上是两个不同的东西。

3. 堆的对象,会在栈的localvariable保存一个指向对象的指针但是cpu高速缓存就是把这个对象直接复制一个备份到高速缓存中,所以一个对象两个备份,就会存在一个线程(cpu)修改后其他线程(cpu)不知道的问题。volatile就是解决这个问题的,强制线程(cpu)从主内存获取最新的值。

试想一下,如果cpu高速缓存也是保存指向堆的指针,那么是不会有并发问题的,因为都是操作同一个对象

原子性

原子性问题的原因:jvm虚拟机中字节码指令因为上下文切换引起的指令交错。在jvm虚拟机中一条修改值的java指令通常分为多个字节码指令,比如i++分为四个,当a线程没有来的及将修改后的值(比如是1)存入变量后被上下文切换到了b线程时,不管b线程中是怎么操作,接下来执行a线程存值后变量的值仍然是1

可见性

可见性问题的原因:由于线程优先从工作内存中获取数据,会出现其他线程对主内存的变量已经做了修改,但是当前线程却还是使用的变量之前的复制值。

可见性和原子性

可见性问题发生在共享变量在线程间一个写,一个/多个读的情况;原子性问题发生在共享变量在线程间的多个写的情况,后者范围更大,因为写操作要先读

可见性只能保证一个线程在获取到变量的时候是其他线程上一瞬间修改过的最新值,但是原子性是指线程间的字节码指令交错。线程一的获取变量的字节码指令获得到最新变量是可见性,往下对变量的修改,线程二的加入属于指令交错。可见性不能解决指令交错。

实事求是地说, 使用字节码来分析并发问题仍然是不严谨的, 因为即使编译出来只有一条字节码指令, 也并不意味执行这条指令就是一个原子操作。一条字节码指令在编译或解释的过程中要用到好几道指令。

有序性(我们说的有序性一般指jvm编译器的重排序

有序性问题的原因:在单线程程序结果不能改变的情况下对不存在数据依赖关系的操作做重排序,结果导致多线程时共享变量在一个线程中由于重排序导致在另一个线程中不能正确读取。

重排序:

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。重排序分3种类型

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

2、指令的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

 

不管怎么重排序,单线程程序的执行结果不能被改变。编译器、运行时和处理器都必须遵守“as-if-serial”语义。

排序会引发什么问题?

在单线程程序中,重排序并不会影响程序的运行结果,而在多线程场景下会破坏多线程程序的语义。

as-if-serial是什么?

不管怎么重排序,单线程程序的执行结果不能被改变。 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉单线程程序是按程序的顺序来执行的as-if-serial语义使单线程程序员无需担心重排序会干扰他们。

但是对于没有正确同步的多线程,这种符合asifserial的重排序还是会影响到线程的安全(因为没有符合hb规则)

happens-before是什么?

先行发生原则,一个操作happens-before另一个操作,表示第一个的操作结果对第二个操作可见,并且第一个操作的执行顺序也在第二个操作之前。

但Java虚拟机不一定按照这个顺序来执行程序。如果重排序的后的执行结果与按happens-before关系执行的结果一致,Java虚拟机也会允许重排序的发生。

 

 

as-if-serial规则和happens-before规则的区别?

区别: happens-before关系保证了同步的多线程程序的执行结果不被改变,as-if-serial保证了单线程内程序的执行结果不被改变。

相同点:happens-before和as-if-serial的作用都是在不改变程序执行结果的前提下,进行重排序提高程序执行的效率。

 

volatile的作用:

1volatile保证了新值能立即同步到主内存, 以及每次使用前立即从主内存刷新,解决了可见性问题。

2、禁止指令重排,解决了有序性问题。

 

Hotspot JVM Volatile的原理

可见性原理

可见性原理是实现了缓存一致性,即:

 

1每个处理器会通过嗅探总线上的数据来查看自己的数据是否过期,一旦处理器发现自己缓存对应的内存地址被修改,就会将当前处理器的缓存设为无效状态。此时,如果处理器需要获取这个数据需重新从主内存将其读取到本地内存。

2当处理器写数据时,如果发现操作的是共享变量,会通知其他处理器将该变量的缓存设为无效状态。

 

缓存一致性的本质就是因为lock前缀指令,它主要有两个作用:

1将当前处理器缓存的数据刷新到主内存;

2刷新到主内存时会使得其他处理器缓存的该内存地址的数据无效,从而迫使其他线程在使用变量时从主内存更新数据。

 

 

有序性原理

有序性原理是编译器在生成字节码时会插入内存屏障。

 

当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的一个操作不会被编译器重排序到volatile读之前。

当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的一个操作不会被编译器重排序到volatile写之后。

当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

 

不能解决指令交错:

写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去

而有序性的保证也只是保证了本线程内相关代码不被重排序,而指令交错是由CPU时间片调度决定的

 

 

volatile、synchronized的区别?

  • volatile主要是保证内存的可见性。synchronized主要是解决多个线程访问资源的原子性问题。
  • volatile作用于变量,synchronized作用于代码块或者方法。
  • volatile仅可以保证数据的可见性和有序性。synchronized可以保证数据的可见性有序性和原子性。
  • volatile不会造成线程的阻塞,synchronized会造成线程的阻塞。

 

volatilesynchronized

synchronized既可以保证代码块的原子性,也同时保证代码块内变量的可见性,也可以保证有序性。

volatile只能保证可见性和有序性。

 

但用于可见性和有序性时经常使用volatile

①、主要原因:volatile比synchronized简单

②、volatile比synchronized性能要好,因为volatile没有加锁

③、volatile和synchronized同时使用的时候,可以适当的提高效率,比如懒汉式的单例模式(volatile的使用场景分析)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值