Java内存模型与线程
硬件的效率与一致性
- 高速缓存作为内存与处理器之间的缓冲:将运算需要用到的数据复制到缓存中,让运算快速进行,当运算结束后再从缓存同步会内存中,处理器无须等待缓慢的内存读写。
缓存一致性问题:MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocol等。 - 指令重排序:处理器再计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。
Java内存模型
操作 | 作用范围 | 作用内容 |
---|---|---|
lock | 主内存变量 | 把一个变量标识为一条线程独占的状态 |
unlock | 主内存变量 | 把一个处于锁定状态的变量释放出来 |
read | 主内存变量 | 把一个变量的值从主内存传输到线程的工作内存 |
load | 工作内存变量 | 把read操作从主内存中得到的变量值放入工作内存的变量副本中 |
use | 工作内存变量 | 把工作内存中一个变量的值传递给执行引擎 |
assign | 工作内存变量 | 把一个从执行引擎接收到的值赋给工作内存的变量 |
store | 工作内存变量 | 把工作内存中的一个变量的值传送到主内存中 |
write | 主内存变量 | 把store操作从工作内存中得到的变量的值放入主内存的变量中 |
规则:
- read和load,store和write必须顺序执行,但不一定连续
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃最近的assign操作
- 不允许一个线程无原因地(没有发生任何assign操作)把数据从线程地工作内存同步回主内存
- 一个新变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,对一个变量实施use、store之前必须先执行assign和load操作
- 一个变量在同一时刻只允许一条线程对其进行lock操作,同一线程可以lock多次
- 如果对一个变量执行lock操作,会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign初始化变量的值
- 如果一个变量没被lock,不允许对它unlock,也不允许unlock一个被其他线程锁定住的变量
- 对一个变量执行unlock之前,必须把变量同步会主内存
volatile
volatile变量特性:
- 此变量对所有线程的可见性,当一条线程修改了这个变量的值,新值对于其他线程来说可以立即得知
- 在不符合以下两条规则的运算场景中,需要加锁保证原子性
- 运算结果不依赖于变量的当前值,或者能确保只有单一线程修改变量的值
- 变量不需要与其他的状态变量共同参与不变约束
- 禁止指令重排序优化,内存屏障,指令重排序时不能把后面的指令重排序到内存屏障之前的位置,增加了lock指令,将本CPU的cache写入内存,也引起其他cpu无效化其cache,相当于做了一次store和write操作,使修改立即可见,同时,把修改同步到内存时,意味着之前所有操作都已经完成,指令重排无法越过内存屏障
性能:
- 读操作与普通变量几乎没差别,写操作可能会慢一些
- 大多场景下,总开销比锁低
规则:
- use和load、read相关联,必须连续一起出现
- assign和store、write相关联,必须连续一起出现
- 对两个volatile变量V和W,如果线程T对变量V的use或assign先于线程T对变量W的,那么线程T对变量V的read或write先于对W的
long和double的非原子协定
允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性
实际虚拟机大部分不会这么实现
原子性、可见性与有序性
原子性:synchronized
可见性:volatile、synchronized(对一个变量执行unlock之前,必须先把此变量同步到主内存中)、final(被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this引用传递出去,那在其他线程中就能看见final字段的值)
有序性:volatile、synchronized(一个变量在同一时刻只允许一条线程对其进行lock)
先行发生原则
- 程序次序规则
- 管程锁定规则
- volatile变量规则
- 线程启动规则
- 线程终止规则
- 线程中断规则
- 对象终结规则
- 传递性
Java与线程
实现线程的方式
方式 | 模型 | 优点 | 缺点 |
---|---|---|---|
使用内核线程实现 | 一对一的线程模型 | 每个轻量级进程成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作 | 基于内核线程实现,各种线程操作都需要进行系统调用,代价高,需要在用户态和内核态来回切换,每个轻量级进程都需要有一个内核线程的支持,要消耗一定的内核资源,一个系统支持轻量级进程的数量是有限的 |
使用用户线程实现 | 一对多的线程模型 | 不需要系统内核支援,在用户态完成,不需要切换到内核态,快速且低消耗,可以支持规模更大的线程数量 | 所有线程操作需要用户程序自己处理,使用用户线程实现的程序一般比较复杂 |
使用用户线程加轻量级进程混合实现 | 多对多模型 | 用户线程操作廉价,可以支持大规模用户线程并发,轻量级进程作为用户线程和内核线程之间的桥梁,可以使用内核提供的线程调度功能及处理器映射,用户线程的系统调用通过轻量级进程完成,降低了整个进程被阻塞的风险 |
Java线程调度
调度方式 | 线程执行时间 | 优点 | 缺点 |
---|---|---|---|
协同式线程调度 | 线程本身控制执行时间,主动通知系统切换线程 | 实现简单 ,线程执行完才会进行线程切换,切换操作对线程自己可知,没有线程同步问题 | 线程执行时间不可控 |
抢占式线程调度 | 系统分配执行时间 | 线程执行时间系统可控,不会有一个线程导致整个进程阻塞的问题 |
状态转换
状态 | 说明 | 进入方式 |
---|---|---|
新建(New) | 创建后尚未启动 | |
运行(Runnable) | 包含操作系统状态中的Running和Ready,可能正在执行或等待CPU为它分配执行时间 | |
无限期等待(Waiting) | 不会被分配CPU执行时间,等待被其他线程显式唤醒 | 没有设置Timeout参数的Object.wait()方法、没有设置Timeout参数的Thread.join()方法、LockSupport.park()方法 |
限期等待(Timed Waiting) | 不会被分配CPU执行时间,无须等待被其他线程显式唤醒,在一定时间后由系统自动唤醒 | Thread.sleep()方法、设置了Timeout参数的Object.wait()方法、设置了Timeout参数的Thread.join()方法、LockSupport.parkNanos()方法、LockSupport.parkUtil()方法 |
阻塞(Blocked) | 被阻塞 | 程序等待进入同步区域 |
结束(Terminated) | 结束执行 |
版权声明:本文为buxiangqumingzi原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。