文章目录
1.Java内存模型的“底层原理”
-
从Java代码到CPU指令的变化过程?
- 最开始,我们编写的Java代码,即
*.Java文件
- 在编译Javac命令后,从刚才的*.Java文件会变出一个新的Java字节码文件,即
*.class文件
- JVM会执行刚才生成的*.class字节码文件,并把字节码文件转化为
机器指令
- 机器指令可以直接在CPU上执运行,也就是
最终的程序执行
- 最开始,我们编写的Java代码,即
-
而不同的JVM实现会带来不同的“翻译”,
不同的CPU平台的机器指令干差万别
,所以我们在Java代码层写的各种Lock,其实最后依赖的是JVM的具体实现(不同版本会有不同实现)和CPU的指令,才能帮我们达到线程安全的效果。 -
Java虚拟机规范中试图定义一种
Java内存模型
(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
。
2.JVM内存结构 VS Java内存模型 VS Java对象模型
- JVM内存结构,和Java虚拟机的
运行时数据区域
有关。 - Java内存模型,和Java
并发编程
有关。 - Java对象模型,和Java对象在
虚拟机中的表现形式
有关。
JVM内存结构:详情点这里 >>> Java运行时数据区域
Java对象模型:详情点这里 >>> Java对象的创建、内存布局和访问定位
3.JMM是什么?
- 全称:Java Memory Model
- 为什么需要JMM?
- C语言不存在内存模型的概念
- 很多并发操作(很多Native方法)依赖处理器,不同处理器结果不一样
- 无法保证并发安全
- 需要一个标准,让多线程运行的结果可预期
- 是一组
规范
,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。 - 如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同规则的重排序之后,导致
不同的虚拟机上运行的结果不一样
,这就造成了很大的问题。 - volatile、synchronized、Lock等的
原理都是JMM
。 - 如果
没有JMM
,那就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦
的,幸好有了JMM,让我们只需要用同步工具和关键字就可以开发并发程序。 - JMM最重要的三点内容:
重排序、可见性、原子性
。
4.重排序
- 什么是重排序:在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令
并不是严格按照代码语句顺序执行的
,他们的顺序被改变了,这就是重排序。 - 重排序的好处:
提高处理速度
。 - 重排序的3种情况
- 编译器优化:包括JVM,JIT编译器等
- CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排序。
- 内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题。
5.可见性
- 什么是可见性:可见性是指当一个线程修改了共享变量的值,其他线程
能够立即得知这个修改
。 - 为什么会有可见性问题:
CPU有多级缓存,导致读的数据过期
。- 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层。
- 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。
- 如果所有的核心都只用一个缓存,那么也就不存在内存可见性问题。
- 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。
- 所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
- happens-before原则:先行发生原则,
动作A发生在动作B之前,B保证能看见A
,这就是happens-before。- 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。
- 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。
- 线程启动规则:Thread的start()方法先行发生于这个线程的每一个操作。
- 线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测线程的终止。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt()方法检测线程是否中断
- 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。
- 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C。
- volatile关键字:详情点这里 >>> volatile关键字
- synchronized关键字:详情点这里 >>> synchronized关键字
6.原子性
- 什么是原子性:一系列操作,
要么全部执行成功,要么全部不执行
,不会出现执行一半的情况,是不可分割的。 - Java中的原子操作有哪些?
- 除了long和double之外的基本类型(
int、byte、boolean、short、char、float
) - 所有
引用reference的复制操作
,不管是32位的机器还是64位的机器 java.concurrent.Atomic.*包
中所有类的原子操作。
- 除了long和double之外的基本类型(
- long和double的原子性
- 问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、读取错误、使用volatile解决。
- 结论:在32位上的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的。
- 实际开发中:商用Java虚拟机中不会出现,默认保证了原子性。
7.面试问题
1.JMM应用实例:单例模式的几种写法、单例和并发的关系?
详情点这里 >>> 单例模式的9种写法
2.讲一讲什么是Java内存模型?
本篇已提到。
3.volatile和synchronized异同?
本篇已提到。
4.什么是内存原子操作?Java中有哪些原子操作?生成对象的过程是不是原子操作?
本篇已提到。
5.什么是内存可见性?
本篇已提到。
6.64位的double和long写入的时候是原子的吗?
本篇已提到。
笔记来源:慕课网悟空老师视频《Java并发核心知识体系精讲》