深入理解java虚拟机笔记Chapter12
(本节笔记的线程收录在线程/并发相关的笔记中,未在此处提及)
Java内存模型
Java 内存模型主要由以下三部分构成:1 个主内存、n 个线程、n 个工作内存(与线程一一对应)
主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则 – 虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。
- Java内存模型规定了所有的变量都存储在主内存(Main Memory,类比物理内存)。
- 线程间变量值的传递均需要通过主内存来完成
- 每条线程还有自己的工作内存(Working Memory,类比处理器高速缓存)
- 线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝
- 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量
- 不同的线程之间也无法直接访问对方工作内存中的变量
粗略来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分区域。 从更低层次上说,主内存就直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。
一个变量从主内存拷贝到工作内存,再从工作内存同步回主内存的流程为:
|主内存| -> read -> load -> |工作内存| -> use -> |Java线程| -> assign -> |工作内存| -> store -> write -> |主内存|
Java 内存模型中的 8 个原子操作
lock
:作用于主内存,把一个变量标识为一个线程独占状态。unlock
:作用于主内存,释放一个处于锁定状态的变量。read
:作用于主内存,把一个变量的值从主内存传输到线程工作内存中,供之后的 load 操作使用。load
:作用于工作内存,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。use
:作用于工作内存,把工作内存中的一个变量传递给执行引擎,虚拟机遇到使用变量值的字节码指令时会执行。assign
:作用于工作内存,把一个从执行引擎得到的值赋给工作内存的变量,虚拟机遇到给变量赋值的字节码指令时会执行。store
:作用于工作内存,把工作内存中的一个变量传送到主内存中,供之后的 write 操作使用。write
:作用于主内存,把 store 操作从工作内存中得到的变量值存入主内存的变量中。
8 个原子操作的执行规则
有关变量拷贝过程的规则
- 不允许 read 和 load,store 和 write 单独出现
- 不允许线程丢弃它最近的 assign 操作,即工作内存变化之后必须把该变化同步回主内存中
- 不允许一个线程在没有 assign 的情况下将工作内存同步回主内存中,也就是说,只有虚拟机遇到变量赋值的字节码时才会将工作内存同步回主内存
- 新的变量只能从主内存中诞生,即不能在工作内存中使用未被 load 和 assign 的变量,一个变量在 use 和 store 前一定先经过了 load 和 assign
有关加锁的规则
- 一个变量在同一时刻只允许一个线程对其进行 lock 操作,但是可以被一个线程多次 lock(锁的可重入)
- 对一个变量进行 lock 操作会清空这个变量在工作内存中的值,然后在执行引擎使用这个变量时,需要通过 assign 或 load 重新对这个变量进行初始化
- 对一个变量执行 unlock 前,必须将该变量同步回主内存中,即执行 store 和 write 操作
- 一个变量没有被 lock,就不能被 unlock,也不能去 unlock一个被其他线程 lock 的变量
对于volatile型变量的特殊规则(预)
关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制;Java内存模型对volatile专门定义了一些特殊的访问规则,一个变量定义为volatile之后,它将具备两种特性:可见性,禁止指令重排序优化。
接下来先介绍先行发生原则(Happens-Before 规则),在返回来学习volatile
Happens-Before 规则
此处,先提一下可见性问题和有序性问题
通过上图可以发现,Java 线程只能操作自己的工作内存,其对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量。这就有可能会导致可见性问题:
- 因为对于主内存中的变量 A,其在不同的线程的工作内存中可能存在不同的副本 A1、A2、A3。
- 不同线程的 read 和 load、store 和 write 不一定是连续执行的,中间可以插入其他命令。Java 只能保证 read 和 load、store 和 write 的执行对于一个线程而言是连续的,但是并不保证不同线程的 read 和 load、store 和 write 的执行是连续的,如下图:
假设有两个线程 A 和 B,其中线程 A 在写入共享变量,线程 B 要读取共享变量,我们想让线程 A 先完成写入,线程 B 再完成读取。此时即便我们是按照 “线程 A 写入 -> 线程 B 读取” 的顺序开始执行的,真实的执行顺序也可能是这样的:storeA -> readB -> writeA -> loadB,这将导致线程 B 读取的是变量的旧值,而非线程 A 修改过的新值。也就是说,线程 A 修改变量的执行先于线程 B 操作了,但这个操作对于线程 B 而言依旧是不可见的。
那么如何解决这个问题呢?通过上述的分析可以发现,可见性问题的本身,也是由于不同线程之间的执行顺序得不到保证导致的,因此我们也可以将它的解决和有序性合并,即对 Java 一些指令的操作顺序进行限制,这样既保证了有序性,有解决了可见性。
于是乎,Java 给出了一些命令执行的顺序规范,也就是大名鼎鼎 Happens-Before 规则。
根据语义,Happens-Before,就是即便是对于不同的线程,前面的操作也应该发生在后面操作的前面,也就是说,Happens-Before 规则保证:前面的操作的结果对后面的操作一定是可见的。
Happens-Before 规则本质上是一种顺序约束规范,用来约束编译器的优化行为。就是说,为了执行效率,我们允许编译器的优化行为,但是为了保证程序运行的正确性,我们要求编译器优化后需要满足 Happens-Before 规则。
根据类别,我们将 Happens-Before 规则分为了以下 4 类:
- 操作的顺序:
- 程序顺序规则: 如果代码中操作 A 在操作 B 之前,那么同一个线程中 A 操作一定在 B 操作前执行,即在本线程内观察,所有操作都是有序的。
- 传递性: 在同一个线程中,如果 A 先于 B ,B 先于 C 那么 A 必然先于 C。
- 锁和 volatile:
- 监视器锁规则: 监视器锁的解锁操作必须在同一个监视器锁的加锁操作前执行。
- volatile 变量规则: 对 volatile 变量的写操作必须在对该变量的读操作前执行,保证时刻读取到这个变量的最新值。
- 线程和中断:
- 线程启动规则: Thread#start() 方法一定先于该线程中执行的操作。
- 线程结束规则: 线程的所有操作先于线程的终结。
- 中断规则: 假设有线程 A,其他线程 interrupt A 的操作先于检测 A 线程是否中断的操作,即对一个线程的 interrupt() 操作和 interrupted() 等检测中断的操作同时发生,那么 interrupt() 先执行。
- 对象生命周期相关:
- 终结器规则: 对象的构造函数执行先于 finalize() 方法。
volatile 的实现原理
Happens-Before 规则中要求,对 volatile 变量的写操作必须在对该变量的读操作前执行,这个规则听起来很容易,那实际上是如何实现的呢?解决方法分两步:
- 保证动作发生;
- 保证动作按正确的顺序发生。
保证动作发生
首先,在对 volatile 变量进行读取和写入操作,必须去主内存拉取最新值,或是将最新值更新进主内存,不能只更新进工作内存而不将操作同步进主内存,即在执行 read、load、use、assign、store、write 操作时:
- use 操作必须与 load、read 操作同时出现,不能只 use,不 load、read。
- use <- load <- read
- assign 操作必须与 store、write 操作同时出现,不能只 assign,不 store、write。
- assign -> store -> write
此时,我们已经保证了将变量的最新值时刻同步进主内存的动作发生了,接下来,我们需要保证这个动作,对于不同的线程,满足 volatile 变量的 Happens-Before 规则:对变量的写操作必须在对该变量的读操作前执行。
保证动作按正确的顺序发生
其实,导致这个执行顺序问题的主要原因在于,这个读写 volatile 变量的操作不是一气呵成的,它不是原子的!无论是读还是写,它都分成了 3 个命令(use <- load <- read 或 assign -> store -> write),这就导致了,你能保证 assignA 发生在 useB 之前,但你根本不能保证 writeA 也发生在 useB 之前,而如果 writeA 不发生在 useB 之前,主内存中的数据就是旧的,线程 B 就读不到最新值!
所以,我觉得这句话应当换一个理解方式:假设我是一个写操作,你发生在我之前的读操作可以随便执行,各个分解命令先于我还是后于我都无所谓。但是,你发生在我之后的读操作,必须等我把 3 个命令都执行完,才能执行!不许偷偷把一些指令排到我的最后一个指令的前面去。 这才是 “对变量的写操作必须在对该变量的读操作前执行” 的本质。
volatile 的真实实现
那么 Java 是如何利用现有的工具,实现了上述的两个效果的呢?
答案是:它巧妙的利用了 lock 操作的特点,通过 观察对 volatile 变量的赋值操作的反编译代码,我们发现,在执行了变量赋值操作之后,额外加了一行:
lock addl $0x0,(%esp)
这一句的意思是:给 ESP 寄存器 +0,这是一个无意义的空操作,重点在 lock 上:
- 保证动作发生:
- lock 指令会将当前 CPU 的 Cache 写入内存,并无效化其他 CPU 的 Cache,相当于在执行了 assign 后,又进行了 store -> write;
- 这使得其他 CPU 可以立即看见 volatile 变量的修改,因为其他 CPU 在读取 volatile 变量时,会发现自己的缓存过期了,于是会去主内存中拉取最新的 volatile 变量值,也就被迫在 use 前进行一次 read -> load。
- 保证动作顺序:
- lock 的存在相当于一个内存屏障,使得在重排序时,不能把后面的指令排在内存屏障之前。
深入理解java虚拟机笔记Chapter12的更多相关文章
- Java内存区域与内存溢出异常——深入理解Java虚拟机 笔记一
Java内存区域 对比与C和C++,Java程序员不需要时时刻刻在意对象的创建和删除过程造成的内存溢出.内存泄露等问题,Java虚拟机很好地帮助我们解决了内存管理的问题,但深入理解Java内存区域,有 ...
- 深入理解Java虚拟机笔记
1. Java虚拟机所管理的内存 2. 对象创建过程 3. GC收集 4. HotSpot算法的实现 5. 垃圾收集器 6. 对象分配内存与回收细节 7. 类文件结构 8. 虚拟机类加载机制 9.类加 ...
- 深入理解java虚拟机笔记Chapter7
虚拟机类的加载机制 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类的加载机制. 类加载的时机 J ...
- 深入理解java虚拟机笔记之一
Java的技术体系主要有支撑java程序运行的虚拟机,提供各开发领域接口支持Java API,java编程语言及许多第三方java框架( 如Spring,Structs等)构成. 可以把Java程序设 ...
- 深入理解Java虚拟机笔记——虚拟机类加载机制
目录 概述 动态加载和动态连接 类加载的时机 类的生命周期 被动引用 例子一(调用子类继承父类的字段) 例子二(数组) 例子三(静态常量) 类加载的过程 加载 验证 准备 解析 符号引用 直接引用 初 ...
- 【转载】深入理解Java虚拟机笔记---运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量表,操作 ...
- 深入理解java虚拟机笔记Chapter8
运行时栈帧结构 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法 ...
- 深入理解java虚拟机笔记Chapter2
java虚拟机运行时数据区 首先获取一个直观的认识: 程序计数器 线程私有.各条线程之间计数器互不影响,独立存储. 当前线程所执行的字节码行号指示器.字节码解释器工作时通过改变这个计数器值选取下一条需 ...
- 类文件结构——深入理解Java虚拟机 笔记三
在之前的笔记中记录过,Java程序变成可执行文件的步骤是:源代码-->经过编译变成class文件-->经过JVM虚拟机变成可执行的二进制文件.因此,为了对JVM执行程序的过程有一个好的了解 ...
随机推荐
- Windows 2003 Server远程代码执行漏洞集合
目录 MS08-067 CVE-2017-7269 MS08-067 发布日期:2008/10/22 针对端口:139.445 漏洞等级:高危 漏洞影响:服务器服务中的漏洞可能允许远程执行代码 受影响 ...
- WindowsPE 第五章 导出表
导出表 PE中的导出表存在于动态链接库文件里.导出表的主要作用是将PE中存在的函数导出到外部,以便其他人可以使用这些函数,实现代码重用. 5.1导出表的作用 代码重用机制提供了重用代码的动态链接库,它 ...
- Windows核心编程 第九章 线程与内核对象的同步(下)
9.4 等待定时器内核对象 等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象.它们通常用来在某个时间执行某个操作. 若要创建等待定时器,只需要调用C r e a t e Wa i ...
- Win64 驱动内核编程-13.回调监控模块加载
回调监控模块加载 模块加载包括用户层模块(.DLL)和内核模块(.SYS)的加载.传统方法要监控这两者加在必须 HOOK 好几个函数,比如 NtCreateSection 和 NtLoadDriver ...
- vscode 终端操作命令npm报错
错误: 如果没有安装的node.js ,则需要安装. node.js官网下载地址: https://nodejs.org/zh-cn/ 安装node.js 后会看到C:\Users\XXX\AppDa ...
- 对标印度的PostMan,一款中国接口测试软件的崛起
对于我们开发者,Api接口调试一定不陌生.包括我在内,之前进行Api调试时,一直使用的是一款印度的软件Postman.记得刚入手的时候,由于该款软件缺乏中文版本,上手一直比较慢,而且还至少存在如下几个 ...
- 逆向工程初步160个crackme-------2
有了第一个crackme的经验后,这个crackme用了半个小时就验证成功了.(思路和第一个crackme相似) 动态调试工具:ollydbg (2.10) 文件分析工具:PEID (0.95) 同样 ...
- 【敏杰开发】Beta阶段项目展示
[敏杰开发]Beta阶段项目展示 项目相关地址汇总 线上地址:http://roadmap.imcoming.top 前端仓库:https://github.com/MinJieDev/Roadmap ...
- Linux创建RAID5_实战
Linux创建RAID5实战 Linux创建RAID5 RAID5最少由三个硬盘组成,它将数据分散存储于阵列中的每个硬盘,并且还伴有一个数据校验位,数据位与校验位通过算法能相互验证 RAID5最多能允 ...
- Linux_ACL文件访问控制列表
一.ACL文件访问控制列表 前言 1️⃣:ACL-文件访问控制列表: 2️⃣:ACL可以针对单个用户,单个文件或目录来进行r.w.x的权限设定,特别适用于需要特殊权限的使用情况. 3️⃣:ACL就是可 ...