深入理解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虚拟机》-----第12章 Java内存模型与线程
-
深入理解Java虚拟机(第三版)-14. 线程安全与锁优化
-
深入理解Java虚拟机(第三版)-13.Java内存模型与线程
-
《深入理解Java虚拟机》(三)垃圾收集器与内存分配策略
-
深入理解Java虚拟机-Java内存区域与内存溢出异常(二)
-
《深入理解java虚拟机》一 java内存区域与内存溢出异常
-
深入理解Java虚拟机02--Java内存区域与内存溢出异常
-
深入理解java虚拟机(1)-----Java内存模型与内存溢出异常
-
《深入理解Java虚拟机》-----第12章 Java内存模型与线程