本文为《深入理解Java虚拟机_JVM高级特性与最佳实践·周志明》学习笔记
TPS(每秒事务处理数):代表着1s内服务端平均处理响应的请求总数。
在相同的任务下,TPS越高,代表程序线程并发协调有条不紊,效率高;TPS越小,线程之间频繁征用数据,互相阻塞以及死锁,降低并发能力;
高速缓存:内存与处理器的桥梁,解决了之间读写速度不一致的问题,同时需要保证缓存一致性(遵守相关协议解决:MSI,MESI,MOSI等)。
共享内存多核系统:多路处理器系统每个处理器都有自己的高速缓存,同时又共享同一主内存;
背景: C/CPP是采用了物理硬件和OS的内存模型,JAVA想屏蔽各种硬件和系统的内存访问差异,实现各个平台下都能达到内存访问一致的效果。
目的:定义程序中的各种变量(实例字段,静态字段和构成数组对象的元素不包含局部变量与参数方法,后者是线程私有)的访问规则;为了避免竞争问题;
规定:所有的变量都存储在JVM主内存中,线程的工作内存中保存了该线程使用变量的主内存副本;线程对变量的所有操作(d,w)都必须在工作内存中进行,不能直接读写主内存的数据。线程间的工作内存也相互独立;
原则:以下操作都是原子的,不可再分的(double和long例外);
lock
(锁定):作用主内存变量,把变量标识为一个线程独占状态;
unlock
(解锁):作用主内存变量,把处于锁定状态的变量释放出来;
read
(读取):作用主内存变量,把一个变量的值从主内存传输到线程的工作内存;
load
(载入):作用主内存变量,把read操作从主内存得到的变量值放入工作内存副本中;
use
(使用):作用于工作内存,把工作内存中的变量值传递给执行引擎,每当虚拟机遇到一个使用变量的值的字节码就会执行该操作;
assign
(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行该操作;
store
(存储):作用于工作内存变量,把工作内存中的一个变量的值传递给主内存中,以便随后的write操作使用;
write
(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;
变量从主内存拷贝到工作内存:按序执行read
,load
变量从工作内存同步回主内存:按序执行store
,write
仅需按序执行,不是连续执行例如:
read
a, read
b, load
b, load
a;
规则:
read
,load
,store
,write
单独出现;assign
操作;assign
操作的变量执行store
;lock
操作;同一线程多次lock
需执行相同次数的unlock
操作才能解锁;lock
操作,会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load
或assign
操作初始化变量的值;unlock
一个没有lock
的变量,不允许unlock
一个其他线程线程锁定的变量;unlock
操作之前,必须先把此变量同步回主内存中(执行store
,write
操作);简化:Java内存模型的操作简化为,read
,write
,lock
,unlock
;只是语言描述上的简化,Java内存模型的基础设计并未改变;
作用:1. 保证此变量对所有线程的可见性(一条线程修改了变量值,其他线程可 以立即得知);2. 禁止指令重排序优化;
保证可见性:1. 运算结果不依赖变量的当前值,或者能够确保只有单一的线程修 改变量的值;2. 变量不需要与其他的状态变量共同参与不变约束;
class Test {
volatile boolean flag;
public void shutdown() {
flag = true;
}
public void doSometings {
while (!shutdown()) {
// todo
}
}
}
禁止重排序: 有volatile修饰的变量赋值后,字节码多了一个lock add$0x0, (%esp)
(空操作),该操作相当于内存屏障,重排序时不能将后面的指令重排序到内存屏障中前的位置。多的指令由于IA32规范中规定lock前缀不允许使用nop指令,该指令的作用是将本处理器缓存写入内存,该动作引起别的处理器或者别的内核无效化其缓存,这种操作相当于对缓存中的变量做了一次类似store和write操作,可以使volatile变量的修改对其他处理器立即可见。
java内存模型对volatile特殊规则:
线程对变量的执行流:load后面是use,线程对变量的use动作可认为是和线程对变量的read,load动作相关联且连续一起出现;
在工作内存中,每次使用变量前都必须从主内存刷新最新的值,用于保证能看见其他线程对变量V所作的修改;
线程对变量的执行流:assign的后一个动作是store,线程对变量的assign动作可认为是对变量store,write相关联必须连续且一起出现;
在工作内存中,每次修改变量后都必须立刻同步回主内存,用于保证其他线程可以看到自己对变量V所作的修改;
如果线程1变量A实施use或assign,再对变量B实施use或assign;那么对变量A实施read或write,再对变量B实施read或write;
保证volatile修饰的变量不会对指令重排序优化,保证代码的执行顺序和程序的执行顺序相同;
背景:Java内存模型对8中操作都具有原子性,但对于64位的数据类型可以由虚拟机自行选择是否保证load,store, read,write四个操作原子性。
现状:主流的64位JVM并不会出现非原子性访问行为,常用的32位HotSpot虚拟机,对long类型的数据存在非原子性访问的风险。在实际开发中,除非明确知道会发生线程竞争,一般不需要因为这个原因刻意的把long和double变量专门声明为volatile。
由Java内存模型保证的原子性变量操作包括 read,load, assign, use, store, write六个。
当一个线程修改了共享变量的值,其他线程能够立即得知此修改。Java内存模型是通过变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存保证的。只不过被volatile
修饰的变量,新值能立即同步到主内存,以及立即从主内存刷新。synchronized
与final
都可实现可见性。
synchronized
实现可见性
由于unlock前必须先把此变量同步回主内存中;
final
实现可见性
被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去,那么其他线程中就能看见final字段的值。
一条线程内,所有的操作都是有序的,若一个线程观察另一个线程,所有的操作都是无序的;
volatile保证线程之间操作有序性
volatile本身包含了禁止指令重排序;
synchronized保证线程之间操作有序性
一个变量同一时刻只允许一条线程对其进行lock操作;
是判断数据是否存在竞争,线程是否安全的判断原则;
定义:Java内存模型中定义的两项操作之间的偏序关系,若A操作先于B发生,那么A操作的影响能被B观察到。以下是无需任何同步手段就能成立的先行规则。
主要方式:内核线程实现(1:1实现),用户线程实现(1:N实现),使用用户线程加轻量级进程混合实现(N:M实现);
内核线程(KLT),直接由OS内核支持的线程,每个线程可以视为内核的分身;程序一般通过内核线程的高级接口——轻量级进程(LWP)去使用内核线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持 内核线程,才能有轻量级进程(1:1)。
优势:
不足:
用户线程实现的方式被称为1:N实现;用户线程是指完全建立在用户空间的线程库上,系统内核不会感知到用户线程的存在及如何实现。
优势:
不足:
将内核线程与用户线程一起使用的实现方式(N:M);既存在用户线程,也存在轻量级进程。
优势:
主流的JVM线程模型普遍被替换成基于OS原生线程模型来实现,即采用1:1的线程模型;
线程调度:系统为线程分配处理器使用权的过程;
调度方式:协同式线程调度,抢占式线程调度;
Java线程调度方式:抢占式线程调度;
概念:协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程主动通知系统切换另一个线程;
优势:实现简单;调度交由线程本身处理;一般可以避免同步问题;
缺陷:线程执行时间不可控,线程阻塞后无法交出处理器;
概念:抢占式调度的多线程系统,由系统分配线程的执行时间,线程调度由系统处理;
优势:线程执行时间是系统可控,不会因为一个线程阻塞导致整个系统阻塞;
因为线程是是交给操作系统去管理,线程的优先级也是由操作系统上的原生线程来实现,最终的调度是由操作系统说了算的,不同系统的优先级不一样,且部分操作系统可自行改变优先级,因此不能通过优先级来判断线程的执行顺序;
java语言 定义了6种状态,一个线程只能有其中的一种状态;
协程:能够协同式调度的线程;
分类:有栈协程、无栈协程;
有栈协程:能够完整的做栈的保护,恢复工作;
无栈协程:无栈协程本质是一种有限状态机,状态保存在闭包种,是更轻量级的协程;例如某些语言的await
,async
,yield
;
java的纤程(有栈协程):可以使用 Quasar里的Fiber 去实现,在字节码层面将线程运行换成 Quasar调度器处理;