Java内存模型(JMM)
为什么要有Java内存模型
不同的操作系统和硬件有不同的对内存的访问策略,而Java是跨平台语言,必须保证Java程序在不同环境下对内存的访问规则一致。为此,JVM协会设计了一套虚拟机规范,用于屏蔽不同系统不同硬件之间的内存访问差异,不同平台的JVM需要实现这套规范,以确保相同的并发代码在不同的平台有相同的表现。
主内存与工作内存
- 主内存和工作内存的最大的区别在于主内存所有线程可见,而工作内存则是线程私有。这样的抽象方式屏蔽了硬件和操作系统的实现细节而高度概括了JVM所关注的关键差异
- 主内存和工作内存是对硬件内存的高度抽象,数据JMM定义内容,不严格对应任何真实硬件
内存的交互操作
| 指令 | 作用于 | 说明 | 备注 |
|---|---|---|---|
| lock | 主内存 | 给主内存变量加锁,使其成为线程独占状态 | lock会清空工作内存中此变量的值,线程要使用需要重新read和load或者use、assign; |
| unlock | 主内存 | 给主内存变量加锁,脱离线程独占状态 | 不能unlock没有被lock的变量,也不能unlock一个被其他线程lock的变量;unlock一个变量之前,必须先将其同步会主内存 |
| read | 主内存 | 读取主内存变量到工作内存 | read和load同时使用 |
| load | 工作内存 | 加载read到的变量至工作内存 | read和load同时使用 |
| use | 工作内存 | 将工作内存中的变量的值传递给执行引擎,jvm遇到需要使用变量的值的时候都需要执行该指令 | |
| assign | 工作内存 | 将执行引擎接收到的值赋值给工作内存的变量 | assign过的变量必须同步会主内存;assign过的变量必须同步会主内存 |
| store | 工作内存 | 将工作内存中变量的值传递到主内存 | store和write必须同时使用 |
| write | 主内存 | 将store传递的值写入主内存变量 | store和write必须同时使用 |
注意事项:
- read和load、store和write禁止单独出现
- assign过的变量必须同步会主内存
- 未经assign的变量禁止同步至主内存
- 新变量只能在主内存中诞生,工作内存不允许使用未初始化的变量
- 一个变量同一时刻只能由一条线程对其进行lock,但lock可重复执行多次,多次lock后必须执行相同次数的unlock变量才能解锁
- lock一个变量将会清空工作内存中此变量的值,若线程要使用需要重新read和load或者use、assign
- 不能unlock没有被lock的变量,也不能unlock一个被其他线程lock的变量
- unlock一个变量之前,必须先将其同步会主内存
PS:
- 以上变量指静态变量、队中的对象等所有线程可见的所有的共享数据,局部变量等线程安全的变量不在范围内
对于volatile性变量的特殊规则
被volatile修饰的变量的特点
- 可见性:一个线程修改了被
volatile修饰的变量,其它线程立即可知 - 有序性:禁止指令重排序优化
Java内存模型对被volatile修饰的变量的特殊规则
- 对于变量V,必须先read&load之后才能使用use,确保确保线程都能从主内存中拿到最新值
- 对于变量V,assign之后必须有store&write,确保线程对变量的修改能实时同步到主内存
- 如果对变量V的use和assign先于对变量W的use和assign,那么对变量V的read/load和store/write先于对变量W的read/load和store/write,确保了该变量不会被指令重排序优化
其他
可以保障volatile修饰的变量线程安全的场景
- 若运算结果不依赖变量的当前值,或能够全部只有单一线程修改变量的值
- 变量不需要于其他的状态变量共同参与不变约束
volatile的实现bug
jdk1.5之前,被volatile修饰的变量不能保证可见性,后该bug在jdk1.5版本中被修复,也是这个bug导致了双检锁实现的单例模式在1.5之前的版本中不能保证单例
内存屏障
通过加锁一个空操作使缓存失效
对于long和double型变量的特殊规则
以上提到的8个内存交互指令,都是具有原子性的,但对于long和double等64位的数据,jvm允许将没被volatile修饰的long和double变量分成两次32位的操作,也就是JVM规范不保证其原子性,不过目前主流的商用JVM实现都保障了long和double的原子性
原子性、可见性和有序性
原子性
什么是原子性
不可分割的,要么一起成功要么一起失败的操作叫做原子性操作
Java中有哪些操作是原子性的
readloadassignusestorewrite满足原子性操作,可以认为基本变量的操作都满足(double和long类型JVM规范不强制要求满足,但JVM实现上大多做到了满足)synchronized可保证原子性
可见性
什么是可见性
指一个线程可以实时且正确地看到另一个线程对变量地修改
哪些方案解决了可见性问题
synchronizedfinalvolatile解决了变量可见性问题
有序性
什么是有序性
特指多线程环境下,指令执行有序,不会得到错误地结果
解决方案
synchronizedvolatile解决了有序性问题
先行发生原则
定义
- JVM规范中定义了确保多线程环境下执行的有序性,设计了一系列规则,符合规则的场景是,程序编译运行时将会保证这些定义场景的先后次序。
规则
- 程序次序规则:在一个线程内,写在前面的操作均先行发生于写在后面的操作
- 管程锁定规则:unlock操作先行发生于后面对同一个锁的lock操作
- 对volatile变量的特殊规则:
volatile变量的写操作先行发生于对其的读操作 - 线程启动规则:线程的
start()方法先行发生于该线程的每一个动作 - 线程终止规则:线程的所有动作都先行发生于对该线程的终止检测,如
Thread.join()、Thread.isAlive() - 线程中断规则:线程的
interrupt()方法先行发生于线程代码检测到中断事件发生 - 对象终结规则:一个对象的初始化操作
new先行发生于其finalize() - 传递性:若A先行发生于B,且B先行发生于C,则A先行发生于C
Java与线程
并发和多线程的关系
多线程不等于并发,是并发的实现方案之一,PHP中还有以多进程实现的并发方案。
线程的实现
- 内核线程:由操作系统内核支持的线程,操作系统会提供“轻量级进程”作为高级接口供程序使用。优点是底层提供支持,效率高,缺点是来回在用户态和内核态之间切换的底层操作太消耗硬件资源。一个轻量级进程对应一个内核线程
- 用户线程:广义指除了内核线程以外的线程,狭义指完全建立在用户空间,内核不能感知的线程存在的实现(即完全由框架实现的线程)。一个CPU对应多个用户线程。优点是用户线程完全就在用户态中,不需要切换内核态,更快速且低消耗。缺点是没有内核支持,实现复杂。
- 用户线程与轻量级进程混合:用户线程执行任务,轻量级进程链接内核线程和用户线程。混合实现的方式平衡了两者的优缺点。
Java线程的实现
- 1.2之前完全使用用户线程,1.2使用了内核线程,当前版本实现方式由操作系统决定,仅对并发规模和操作成本有影响,对程序来说感知不到差异
Java线程调度
- 协同式线程调度:由线程控制执行时间,执行完毕需要主动通知系统切换线程
- 优点:
- 实现简单
- 并发问题相对更可控
- 缺点
- 不通知的话可能一直阻塞
- 优点:
- 抢占式线程调度
- 优点
- 线程执行时间系统可控,不会一直阻塞
- 优点
- 线程优先级
- 不强制地软性地控制线程被执行地可能。仅作参考,不是绝对优先
线程状态
graph LR
A[NEW] -->|new| B[RUNABLE]
C -->|IO完成或锁已释放| B
G -->|IO阻塞或没拿到锁| C[BLOCKED]
B -->|得到权限| G[RUNNING]
G -->|yield| B
G -->|结束| D[Dead]
G -->|wait,sleep,join,LockSupport.park| E[WAITING]
G -->|wait,sleep,join,LockSupport.parkNanos,LockSupprt.parkUtil| H[WAITING,TIMED_WAITING]
E -->|notify,notifyAll,LockSupport.unpark| B
H -->|到时限,LockSupport.unpark| B
文档信息
- 本文作者:Ling He
- 本文链接:https://GoggleHe.github.io/2024/02/28/JMM/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)