Java内存模型(JMM)

2024/02/28 并发 共 3367 字,约 10 分钟

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中有哪些操作是原子性的
  • read load assign use store write 满足原子性操作,可以认为基本变量的操作都满足(double和long类型JVM规范不强制要求满足,但JVM实现上大多做到了满足)
  • synchronized可保证原子性
可见性
什么是可见性

指一个线程可以实时且正确地看到另一个线程对变量地修改

哪些方案解决了可见性问题
  • synchronized final volatile 解决了变量可见性问题
有序性
什么是有序性

特指多线程环境下,指令执行有序,不会得到错误地结果

解决方案
  • synchronized volatile 解决了有序性问题

先行发生原则

定义
  • 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

文档信息

Search

    Table of Contents