JMM模型
定义:
Java内存模型(JMM)是一种抽象的概念,不是 JVM内存模型
组成:
- 线程
- 工作内存 —— 线程私有的一块内存空间 也称栈空间
- 主内存 —— **共享内存区域。**所有线程都能访问
- 总线 —— 完成主内存和工作内存之间的数据交互的桥梁
- 执行引擎 —— CPU执行操作
JMM存在的必要性
工作内存和主内存数据如何交互?
JMM定义了以下数据同步八大原子操作来实现
八大原子操作
指令 | 行为 | 位置 |
---|---|---|
read | 主内存拷贝一份变量的数据副本 | 主内存 |
load | 将数据副本加载到工作内存 | 主内存->工作内存 |
use | 执行引擎使用给数据副本 | 工作内存->执行引擎 |
assign | 执行引擎赋值给数据副本 | 执行引擎->工作内存 |
store | 将数据副本传回主内存 | 工作内存->主内存 |
write | 将数据副本赋值给原变量 | 主内存 |
lock | 主内存变量标记为线程独占 | 主内存 |
unlock | 主内存变量取消线程独占 | 主内存 |
数据交互顺序:
- 主内存变量 read 生成数据副本
- 数据副本 load 进工作内存
- 执行引擎 use 数据副本
- 执行引擎 assign 给数据副本
- 数据副本 store 到主内存
- 主内存数据副本 write 给原变量
数据交互中可能产生的问题
因为工作内存是线程私有的,而主内存的资源是共享的,在多线程并发的情况下,就有可能导致脏写【更新覆盖】的问题
怎么解决?
首先需要了解,并发编程的三大特性
并发编程的三大特性
原子性
原子性是指:**一个操作不可中断,**即使在多线程情况下,一个操作一旦开始就不会被其他线程影响。
Java规定**对于基本数据类型的读写操作是原子的**
可见性
可见性是指:一个线程修改了某个共享变量的值,其他线程能够马上得知这个修改的值。
在多线程情况下,线程A修改了共享变量X的值,而还未写回主内存时,此时线程B读到共享变量X的值【仍为旧值】,这种 工作内存与主内存同步延迟现象 造成了可见性问题。
有序性
因为指令重排序【性能优化】,导致在多线程情况下有可能导致指令顺序未必一致的问题
四个问题
怎么解决原子性问题?
可以通过加锁的方式来实现原子性
- synchronized
- Lock
加锁可以保证 任一时刻只有一个线程访问该代码
怎么解决可见性问题?
-
volatile 关键字保证可见性
他能保证共享变量x的值被修改后能立即被其他线程看到
-
加锁可以保证可见性
因为加锁可以保证 任一时刻只有一个线程访问该代码,释放锁之前,共享变量的值被刷新回主内存
怎么解决有序性问题?
-
volatile 关键字保证有序性
禁止指令重排序,从而保证有序性
-
加锁可以保证可见性
因为加锁可以保证 任一时刻只有一个线程访问该代码,相当于单线程环境下,自然有序
什么是指令重排序?
只要程序的最终结果与JVM顺序化情况的结果相等,那么指令的执行顺序 可以 与代码顺序不一致,此过程叫指令重排序 【为了符合CPU特性,增强性能】
指令重排序遵循as-if-serial语义和happens-before原则
as-if-serial语义
不管怎么重排序,程序执行结果都不能被改变。
happens-before 原则
规则 | 解释 |
---|---|
程序顺序原则 | 在一个线程内必须保证语义顺序执行 【可重排】 |
锁规则 | 如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后 |
volatile规则 | volatile变量的写,先发生于读【读最新值】 |
线程启动规则 | 如果线程A在执行线程B的start()之前修改了共享变量的值,那么当线程B执行start()时,线程A对共享变量的修改对线程B可见 |
传递性 | A先于B ,B先于C 那么A必然先于C |
线程终止规则 | 线程的所有操作先于线程的终结 |
线程中断规则 | 先调用线程interrupt(),再检测中断事件的发生interrupted() |
对象终结规则 | 对象的构造函数执行,结束先于finalize()方法 【GC】 |