2Java内存模型底层原理.png

思维导图

什么叫底层原理

Java代码到CPU指令

  1. 最开始我们编写的是Java代码,是*.java文件
  2. 在编译(javac命令)后,从刚开始的.java文件会变成一个新的Java字节码文件(.class)
  3. JVM会执行刚才生成的字节码文件(*.class),并把字节码文件转化为机器指令
  4. 机器指令可以直接在CPU上执行,也就是最终的程序执行

JVM实现会带来不同“翻译”,无法保证并发安全效果的一致

而不同的JVM实现会带来不同的“翻译”,不同的CPU平台的机器指令又千差万别;所以我们在java代码层写的各种Lock,其实最后依赖的是JVM的具体实现(不同版本会有不同实现)和CPU的指令,才能帮我们达到线程安全的效果。
由于最终效果依赖处理器,不同处理器结果不一样,这样无法保证并发安全,所以需要一个标准,让多线程运行的结果可预期,这个标准就是JMM。

重点开始向下转移:转化过程的规范和原则

class字节码

JVM内存结构 VS Java内存模型 VS Java对象模型

JVM内存结构

Java虚拟机的运行时区域,分为堆、虚拟机栈、方法区、本地方法栈、程序计数器

截屏2020-01-22下午1.26.10.png

Java对象模型

Java对象在虚拟机中的表现形式
Java是面向对象的,每个对象在JVM中存储是有一定结构的,而结构这个模型就叫做Java对象模型
截屏2020-01-22下午1.31.07.png

  • Java对象自身的存储模型
  • JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类
  • 当我们在Java代码中,使用new 创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据

Java内存模型

为什么需要JMM

  1. C语言不存在内存模型的概念
  2. 依赖处理器,不同处理器结果不一样
  3. 无法保证并发安全
  4. 需要一个标准,让多线程运行的结果可预期

JMM是规范

  1. Java Memory Model ,缩写JMM
  2. 是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便的开发多线程程序
  3. 如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同虚拟机上运行的结果不一样,那是很大的问题

JMM是工具类和关键字的原理

  1. volatile、synchronized、Lock等的原理都是JMM
  2. 如果没有JMM,那就需要我们自己指定什么时候用内存栅栏(工作内存和主内存的拷贝和同步)等,那是相当麻烦的,幸好有了JMM,让我们只需要用同步工具和关键字就可以开发并发程序

最重要的3点内容

重排序、可见性、原子性

重排序

代码实际执行的顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序

重排序的好处:提高处理速度

截屏2020-01-22下午2.46.55.png

重排序的3种情况

编译器优化:包括JVM,JIT编译器等
编译器(包括JVM,JIT编译器等)出于优化的目的(例如当前有了数据a,那么如果把对a的操作放到一起效率会更高,避免了读取b后又返回来重新读取a的时间开销),在编译的过程中会进行一定程度的重排,导致生成的机器指令和之前的字节码的顺序不一致。

CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排
CPU 的优化行为,和编译器优化很类似,是通过乱序执行的技术,来提高执行效率。所以就算编译器不发生重排,CPU 也可能对指令进行重排,所以我们开发中,一定要考虑到重排序带来的后果。

内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题
内存系统内不存在重排序,但是内存会带来看上去和重排序一样的效果,所以这里的“重排序”打了双引号。由于内存有缓存的存在,在JMM里表现为主存和本地内存,由于主存和本地内存的不一致,会使得程序表现出乱序的行为。

可见性

什么是可见性问题

其他线程看不见共享变量的变化

为什么会有可见性问题

截屏2020-01-22下午4.06.47.png
CPU有多级缓存,导致读的数据过期
1)高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层
2)线程间对于共享变量的可见性问题不是直接由多核引起的,而是由多级缓存引起的
3)如果所有核心都只用一个缓存,那么也就不存在内存可见性问题了
4)每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致核心读取的值是一个过期的值

JMM抽象:主内存和本地内存

Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念
这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的抽象,是对于寄存器、一级缓存、二级缓存等的抽象
主内存和本地内存图示:

截屏2020-01-22下午4.54.45.png

主内存和工作内存的关系,JVM有以下规定:

  1. 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量是主内存的拷贝
  2. 线程不能直接读写主内存中的变量,而只能操作自己工作内存中的变量,然后再同步到主内存中
  3. 主内存是多个线程共享的,但线程间不能共享工作内存,如果线程间需要通信,必须借助主内存中转来完成

所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题

happens-before原则

什么是happens-before

happens-before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A
两个操作可以用happens-before来确定他们的执行顺序:如果一个操作happens-before于另一操作,那么我们说第一个操作对于第二个操作是可见的

什么不是happens-before

两个线程没有相互配合机制,所以代码X和代码Y的执行结果并不保证总被对方看到的,这就不具备happens-before

happens-before规则有哪些

单线程规则
锁操作(Synchronized和Lock)
volatile变量
线程启动
线程join
传递性:第一行代码对第二行代码可见,第二行对第三行可见,即第一行对第三行可见
中断:一个线程被其他线程interrupt时,那么检测中断(isInterrupted)或者抛出InterruptedException
工具类的happens-before原则
1)线程安全的容器get一定能看到此之前的put等存入动作
2)CountDownLatch
3) Semaphore
4) Future
5) 线程池
6) CyclicBarrier

volatile关键字

volatile是什么?

  • volatile是一种同步机制,比synchronized或者Lock更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为
  • 如果一个变量被修饰成volatile,那么JVM就知道了这个变量可能会被并发修改
  • 但是开销小,相应的能力也小,虽然volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile只在很有限的场景下发生作用

    volatile适用场景

    适用场景1:boolean flag,如果一个变量自始至终只被各个线程赋值,而没有其他操作,那么就可以用volatile代替synchronized或者代替原子变量,因为赋值自身就是原子性的,而volatile又保证了可见性,所以就保证了线程安全

使用场景2:作为刷新之前变量的触发器(hapens-before原则保证,在读取volatile变量时,发生在他之前的操作都是可见的)

volatile的两点作用

可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值;写一个volatile属性会立即刷入到主内存
禁止指令重排序优化:解决单例双重锁乱序问题

volatile和synchronized的关系

volatile可以看做轻量版的synchronized:如果一个变量自始至终只被各个线程赋值,而没有其他操作,那么就可以用volatile代替synchronized或者代替原子变量,因为赋值自身就是原子性的,而volatile又保证了可见性,所以就保证了线程安全

总结

  1. 适用场景,见volatile适用场景
  2. volatile的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以他是低成本的
  3. volatile只能作用于属性即字段上,编译器就不会对这个属性做指令重排序
  4. volatile提供了可见性
  5. volatile提供了happens-before保证
  6. volatile使得long和double的赋值是原子的

能保证可见性的措施

见happens-before规则

synchronized可见性的正确理解

synchronized不仅保证了原子性,还保证了可见性
synchronized不仅让被保护的代码安全,还近朱者赤,即synchronized代码块和synchronized之前的代码,对其他线程都是可见的,如下图所示
截屏2020-01-22下午7.15.10.png

原子性

什么是原子性

一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的

Java中的原子操作有哪些

除了long和double之外的基本类型的赋值操作
所有引用reference的赋值操作,不管32位还是64位机器
java.concurrent.Atomic.*包中所有类的原子操作

long和double的原子性

问题描述:官方文档、对于64位值的写入,可以分为两个32位的操作进行写入;读取错误、用volatile解决
结论:在32位的JVM上,long和double的操作不是原子的,但是在64位上是原子的
在实际的开发中:商用的JVM上已经保证了写入的原子性

原子操作+原子操作!=原子操作

简单的把原子操作组合在一起,并不能保证整体依然具有原子性