深入理解Java虚拟机(第三版)-13.Java内存模型与线程
13.Java内存模型与线程
1.Java内存模型
Java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到主内存和从内存中取出变量值的底层细节
该变量指的是 实例字段、静态字段、和构成数组对象的元素,不包含线程私有的 局部变量和方法参数
Java线程 -》 工作内存 -》 Save 和 load 操作 -》 主内存
每个线程都有自己的工作内存,工作内存中保存了该线程使用的变量的主内存副本,线程对变量的所有操作(读取赋值)都在工作内存中进行,而不能直接读写主内存。
各个线程之间无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存来完成。
2.内存间交互操作,Java内存模型定义了8种操作来完成变量在主内存和工作内存间的交互协议,以下操作均为原子不可再分操作
lock 主内存变量、
unlock 主内存变量、
read 从主内存读取变量值到工作内存,供load使用、
load 将主内存的值放入工作内存的变量副本中、
use 作用于工作内存,将工作内存的值赋值给执行引擎、
assign 作用于工作内存,将执行引擎接收的值赋值给工作内存、
store 作用于工作内存,将工作内存的值传送给主内存,供write使用、
write 作用于主内存,将store操作从工作内存中取出的值赋值给主内存
3.Java 规定了执行上述8种基本操作时必须满足的规则:
不允许read 和 load,store和write 操作之一单独出现,即不允许一个变量从内存读取之后,工作内存不接受。或者工作内存发起回写但主内存不接受
不允许最近一个线程丢弃它最近的assign 操作,即变量在工作内存被改变之后必须把变化同步回主内存
不允许一个线程无原因的(没有发生过assign操作)把数据从线程的工作内存同步回主内存
一个新的变量只能在主内存中”诞生“,不允许在工作内存中直接使用一个未被初始化(load 和 assign)的变量,换句话说,就是对一个变量实施 use store 操作之前
必须先执行 load 和 assign 操作
一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一线程执行多次,多次lock 后只有执行相同次数的unlock 操作,变量才会被解锁
如果对一个变量进行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作以初始化该变量的值
一个变量如果没有lock操作,则不允许对其进行unlock操作,也不允许去unlock一个被其他线程锁定的变量
对一个变量执行unlock操作之前,必须把此变量同步回主内存中(执行store,write操作)
4.以上规则限定等效于先行发生原则:
程序次序规则:在一个线程内,按照控制流顺序,书写在前面的操作现行发生于后面的操作,注意是控制流而不是代码顺序
管程锁定规则:一个unlock操作现行发生于后面对同一个锁的lock操作
volatile规则:对一个volatile变量的写操作现行发生于后面对这个变量的读操作,后面指的是时间上的先后
线程启动原则:线程的start方法先行发生于此线程的所有动作
线程终止原则:线程的所有操作都先行发生于对此线程的终止检测
线程中断原则:
对象终结规则:一个对象的初始化完成(构造函数结果)先行发生于他的finalize方法的开始
传递性:操作A先行与B,B先行于C,则A先行于C
5.对 volatile 型变量的特殊规则
轻量级同步机制
volatile特性:
1. 保证该变量对所有线程可见;
volatile 在各个线程的工作内存中是不存在一致性问题的。从物理存储的角度看,各个线程的工作内存中 volatile 变量也可以存在不一致的问题,
但由于每次使用之前,都要先刷新,执行引擎看不到不一致的情况,因此认为不存在一致性问题。
不存在一致性问题,不代表线程安全。例如自增操作,race++,javap反编译代码后,得到四条字节码指令,在第一条指令getstatic把race的值取到栈顶时,volatile保证了race 的值在此时是正确的,但执行之后的指令时,其他线程可能已经把race的值改变了,而操作栈顶的值就变成了过期的数据,所以 putstatic 执行后,可能把较小的 race 值同步回主内存之中
一条字节码指令可能对应多条本地机器码指令。
volatile 在保证可见性的同时,在以下两种场景下,可同时保证原子性:
1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
2. 变量不需要与其他的状态变量共同参与不变约束
2. 禁止指令重排序;
双锁检测(DCL,Double Check Lock),注意,JDK5 之前,volatile 并不能保证禁止指令重排序,所以,JDK5之前,java无法通过DCL 实现单例模式
加入volatile后,生成的汇编代码,多出了一行 ”lock addl $0x0,(%esp)“操作,这个操作的作用相当于一个内存屏障,指令重排序时,不能把后面的指令重排序到内存屏障之前的位置。
该汇编代码的意思是,将 esp 寄存器的值加0 ,配合lock使用,lock的作用是将本处理器的缓存写入内存,让前面 volatile 变量的修改对其他处理器立即可见。
为什么是禁止指令重排序呢?从硬件架构上讲,指令重排序就是指处理器采用了允许将多条指令不按照程序规定的顺序分开发送给各个相应的电路单元进行处理。但不是任意的重排,处理器必须能正确的处理指令依赖情况保障程序能得出正确的执行结果。
所以,在同一个处理器中,重排过后的代码看起来仍然是有序的,因此lock addl $0x0,(%esp)指令把修改同步到内存时,意味着所有之前的操作已经执行完成,这样便形成了”指令重排无法越过内存屏障“的效果
volatile 性能:
比synchronize 快不多,但volatile读操作比普通遍历读取操作差不多,写操作会慢一点,因为禁止了指令重排,
java内存模型对 volatile 的规则:T标识线程,V/W标识两个volatile变量
1. 在工作内存中,每次使用V前都必须从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改
2. 在工作内存中,每次修改V后都必须立刻同步回主内存,用于保证其他线程可以看到自己对变量V所做的修改
3. volatile修饰的变量不会被指令重排序,从而保证代码执行顺序与程序的顺序相同。
6. 原子性、可见性、有序性;看下Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性三个特征来建立的
1.原子性:
java内存模型直接保证的原子操作:read、load、use、assign、store、write,大致认为基本类型的访问读写都是原子性的。
更大范围的原子性,java内存模型还提供了lock和unlock来满足。尽管虚拟机未把loak和unlock开放给用户,但提供了更高层次的字节码指令 monitorenter和moniterexit来隐士的使用这两个操作。
这两个字节码指令反映到java代码中就是同步块-synchronized关键字,因此 synchronized也具有原子性。
2.可见性:
1. Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论变量是volatile还是普通变量。
区别是:volatile保证新值能立即同步到主内存,以及每次使用前立即从主内存刷新。即volatile保证了多线程操作时变量的可见性。
2. synchronized也具有可见性。”对一个变量执行unlock操作之前,必须把此变量同步回主内存“这一规则保证的
3. final 的可见性。”被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去(this引用逃逸很危险,以为其他线程可以通过这个引用访问到初始化了一半的对象),那么在其他线程中就能看到final的字段值“
3.有序性
1. java 程序天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。
前半句是线程内表现为串行的语义,后半句是指令重排序和工作内存与主内存的同步延迟现象。
2. volatile 本身就包含了禁止指令重排序的语义
3. synchronized 的有序性是通过”一个变量在同一时刻只允许一个线程对其进行lock操作“这条规则获得的
7. java与线程
实现线程主要有三种方式:使用内核线程实现(1:1实现);使用用户线程实现(1:N实现);使用用户线程加轻量级进程混合实现(N:M实现)
1. 内核线程实现
内核线程(KLT)就是直接有操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核成为多线程内核
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口-轻量级进程(通常意义上的线程),由于每个轻量级进程都是由一个内核线程支持,这种轻量级进程与内核线程之间的1:1关系成为一对一的线程模型。
轻量级进程是一个独立的调度单元。
局限性:
1. 各种线程操作,如创建、析构、同步,都需要进行系统调用,而系统调用代价相对交过,需要在用户态和内核态切换
2. 每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的
2. 用户线程实现
广义讲,只要不是内核线程都是用户线程
狭义讲,完全建立在用户空间的线程库上,系统内核感知不到用户线程的存在以及实现。用户线程的建立同步销毁调度完全在用户态完成,不需要内核的帮助。
优势:因为不需要切换到内核态,因此操作非常快且低消耗,也能支持更大规模的线程数量
劣势:实现复杂,例如阻塞问题处理
3. 用户线程加轻量级进程混合实现
用户线程与内核线程共存。
8. java线程的实现
JDK1.3 以后,主流平台上的主流商用java虚拟机普遍使用基于操作系统原生线程模型来实现,即采用1:1的线程模型,例如hotspot
以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以Hotspot不干涉线程调度,全权交给操作系统处理,包括冻结线程、唤醒线程、执行时间分配、线程具体交给哪个处理器核心去执行,都是操作系统决定的
9.java线程调度
线程调度是指系统为线层分配处理器使用权的过程,主要分为协同式线程调度和抢占式线程调度。
1.协同式线程调度
线程自己的工作执行完成之后,要主动通知系统切换到另一个线程。
优势:实现简单,切换可知,无同步问题
劣势:线程实现时间不可控,如果一个进程坚持不让出处理器的执行时间,就可能导致整个系统崩溃
2.抢占式线程调度(java采用该调度方式)
由操作系统分配每个线程的执行时间
优势:不会因为一个线程导致整个进程或系统崩溃
java线程优先级
java线程调度虽然由系统决定,但我们可以”建议“操作系统给某些线程多分配一点时间——通过设置线程优先级来实现。
该技术并不稳定
1. 主流虚拟机上的java线程是被映射到系统的远程线程来实现的,所以最终线程调度系统说了算。尽管操作系统也提供了线程优先级的概念,但不见得能与java线程优先级一一对应。当操作系统优先级少于java,那不同的java优先级会被映射为同一内核线程优先级。
2. 优先级可能会被系统自行改变。当某个线程被执行的特别频繁时,系统可能会越过优先级去为他分配执行是啊金,从而减少线程频繁切换而带来的性能损耗
10.为什么内核线程调度成本高?
内核线程调度成本主要来自于用户态与内核态之间的状态转换,而两种状态转换之间的开销主要来自于响应中断、保护和恢复执行现场的成本。
程序是代码和数据的结合体,代码执行时必须有上下文的支持,而这里的上下文,以程序员的角度讲,是方法调用过程中的各种局部变量与资源;以线程的角度看,是方法的调用栈中存储的各类信息;以操作系统和硬件的角度看,是存储在内存、缓存和寄存器中的一个个具体数值。
物理硬件的各种存储设备和寄存器是被操作系统内所有线程共享的资源,当中断发生,线程切换完成前,系统首先要把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程B挂起时的状态,这样线程B被激活之后才能仿佛没有被挂起过,这样保护和恢复现场的工作,免不了涉及一系列数据在各种寄存器、缓存中的来回拷贝,当然不可能是轻量级的操作。
深入理解Java虚拟机(第三版)-13.Java内存模型与线程的更多相关文章
- 深入理解Java虚拟机第三版,总结笔记【随时更新】
最近一直在看<深入理解Java虚拟机>第三版,无意中发现了第三版是最近才发行的,听说讲解的JDK版本升级,新增了近50%的内容. 这种神书,看懂了,看进去了,真的看的很快,并没有想象中的晦 ...
- Java虚拟机(三):Java 类的加载机制
1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 ...
- Java虚拟机(二):JVM内存模型
所有的Java开发人员可能会遇到这样的困惑?我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?该怎么解决呢?其实如果你经常解决服务器性能问题,那么这些问 ...
- Java虚拟机(一):JVM内存结构
所有的Java开发人员可能会遇到这样的困惑?我该为堆内存设置多大空间呢?OutOfMemoryError的异常到底涉及到运行时数据的哪块区域?该怎么解决呢?其实如果你经常解决服务器性能问题,那么这些问 ...
- Java虚拟机(二):Java GC算法 垃圾收集器
概述 垃圾收集 Garbage Collection 通常被称为“GC”,它诞生于1960年 MIT 的 Lisp 语言,经过半个多世纪,目前已经十分成熟了. jvm 中,程序计数器.虚拟机栈.本地方 ...
- Java虚拟机系列(一)---Java内存划分
Java和C++之间有一堵由内存管理和垃圾收集技术所围成的“高墙”,墙外的人想进去,墙内的人却想出来. ------摘自<深入理解Java虚拟机> 作为一个Java程序员,因为虚拟机的好 ...
- 《深入理解Java虚拟机》-----第2章 Java内存区域与内存溢出异常
2.1 概述 对于从事C.C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对象生命开始到终结的维护责任 ...
- 深入理解Java虚拟机读书笔记8----Java内存模型与线程
八 Java内存模型与线程 1 Java内存模型 ---主要目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节. ---此处的变量和J ...
- 《深入理解Java虚拟机》- Java虚拟机是如何加载Java类的?
Java虚拟机是如何加载Java类的? 这个问题也就是面试常问到的Java类加载机制.在年初面试百战之后,菜鸟喜鹊也是能把这流程倒背如流啊!但是,也只是字面上的背诵,根本就是像上学时背书考试一样. ...
随机推荐
- 《ASP.NET Core 3框架揭秘》读者群,欢迎加入
作为一个17年的.NET开发者,我对一件事特别不能理解:我们的计算机图书市场充斥着一系列介绍ASP.NET Web Forms.ASP.NET MVC.ASP.NET Web API的书籍,但是却找不 ...
- vijos 1449 字符串还原
背景 小K是一位蔚蓝教主的崇拜者(Orz教主er),有一天,他收到了一封匿名信,信告诉了小K由于他表现出色,得到了一次当面Orz教主的机会,但是要当面Orz教主可不那么容易,不是每个人都有资格Orz教 ...
- 【asp.net core】实现动态 Web API
序言: 远程工作已经一个月了,最近也算是比较闲,每天早上起床打个卡,快速弄完当天要做的工作之后就快乐摸鱼去了.之前在用 ABP 框架(旧版)的时候就觉得应用服务层写起来真的爽,为什么实现了个 IApp ...
- vue中eslint报错的解决方案
1,Newline required at end of file but not found. (eol-last) //文末需要一行 这个是报错: 这个是不报错的: 只需要在最后一行加上一空行即可 ...
- 学习RF遇到的问题
1.Windows安装pip命令安装RF报错: File "<stdin>", line 1 pip install robotframework 原因:pip命令不在 ...
- Vue2.0 【第一季】第6节 v-model指令
目录 Vue2.0 [第一季] 第6节 v-model指令 第6节 v-model指令 一.一个最简单的双向数据绑定代码: 二.修饰符 三.文本区域加入数据绑定 四.多选按钮绑定一个值 五.多选绑定一 ...
- springboot项目中thymeleaf布局应用
.katex { display: block; text-align: center; white-space: nowrap; } .katex-display > .katex > ...
- P5596 【XR-4】题 笔记
P5596 [XR-4]题 其实这题我昨天没做出来--所以今天写一下笔记 昨天我还信誓旦旦地说这一定是一道黑题\(OTZ\).果然菜是原罪. 另外吐槽一下科技楼机房频繁停电,昨天写了两小时的树刨和倍增 ...
- Go 武林外传 - 初出茅庐
没有旁白. 我叫小白, 白痴的白. 老头说我太笨了, 提前放我下山, 让我自生自灭. 对了, 忘了说了, 那老头是我师傅. 虽然我的内心深处是拒绝的, 但是我又打不过老头, 只好收拾铺盖滚犊子了. 算 ...
- IOptions、IOptionsMonitor以及IOptionsSnapshot
背景 ASP.NET Core引入了Options模式,使用类来表示相关的设置组.简单的来说,就是用强类型的类来表达配置项,这带来了很多好处.初学者会发现这个框架有3个主要的面向消费者的接口:IOpt ...