文章目录
- 计算机硬件存储体系
- 基于计算机存储结构的 JMM
- Java 内存模型 JavaMemoryModel
- JMM规范下的三大特性
- 原子性
- 可见性
- 有序性
- 多线程对变量的读写过程
- 读取过程
- 多线程先行发生原则 happens-before
- x,y 的 case 说明
- happens-before 原则说明
- happens-before 大原则
- happens-before 的 8 条原则
- 次序规则
- 锁定规则
- volatile 变量规则
- 传递规则
- 线程启动规则(Thread Start Rule)
- 线程中断规则(Thread Interuption Rule)
- 线程终止规则(Thread Termination Rule)
- 对象终结规则(Finalizer)
- Summary
- case
大厂面试题
- 什么是 Java 内存模型?
- JMM 与 volatile 之间的关系?
- JMM 有些特性(JMM 的三大特性是什么)
- 为什么要有 JMM?(JMM 的能干什么?)(JMM 为什么出现?)
- happens-before 先行发生原则是什么?
计算机硬件存储体系
- 计算机存储结构,从本地磁盘到主存,再到 CPU 缓存,也就是硬盘到内存,到 CPU
- 一般对应的程序的操作就是从数据库查数据到内存,然后 CPU 进行计算
- CPU 中集成众多寄存器
基于计算机存储结构的 JMM
- CPU 对内存的读写操作的数据不一致性问题
- CPU 对数据的操作由外存->内存->三级缓存 3 个阶段,对内存读写操作存在数据不一致问题
- question :
- 如何解决因为硬件与操作系统数据读写速度不同导致的数据不一致问题?
- answer :
- JVM 定义了 JMM 用于屏蔽各种硬件与操作系统的内存访问差异,实现 JVM 跨平台达到一致的内存访问效果
- JVM 定义了 JMM 用于屏蔽各种硬件与操作系统的内存访问差异,实现 JVM 跨平台达到一致的内存访问效果
Java 内存模型 JavaMemoryModel
JMM (Java 内存模型 JavaMomary Model,简称 JMM),
- 本身是一种抽象的概念并不真实存在,仅仅描述了一组约定或规范,(本质)
- 通过这组规范定义了程序中(特别是多线程)各个变量的读写访问方式,并决定了一个线程对共享变量的何时写入,以及如何变成对另一个线程可见(作用方式)
- 关键技术点都是围绕多线程的原子性,可见性和有序性展开的(核心,原则)
- 作用
- 实现线程和主内存之间的抽象关系
- 屏蔽底层硬件和操作系统的内存访问差异,实现 JVM 跨平台内存访问的一致性
JMM规范下的三大特性
原子性
- 指的是一个操作不可被打断,多线程环境下,操作不能被其它线程所干扰
可见性
- 当一个线程修改了某个共享变量的值,其他线程是否能够立即知道该变更
- JMM 规范规定所有变量都存储在主内存中
- 读写过程
- 线程 A 先从主内存中读取到一个共享变量到自己的线程域内存中,作为本地共享变量的副本
- 在本地线程域内存对本地共享变量进行修改
- 将修改后的数据写回到主内存中
- 线程 B 对线程 A 的更新进行读取
- 设计思想
- 系统主内存共享变量数据的修改被写入的时机是不确定的,多线程并发下可能存在"脏读"
- 因此,每个线程都有自己的线程域,在线程域中保存共享变量副本
- 线程对变量的所有操作都必须在本地线程域中进行
- 不能直接读写主内存中的变量,不同线程之间也无法直接访问对方线程域中的变量
- 线程间变量值的传递依赖主内存进行实现
- 线程脏读现象
- 可见性基于该现象,实现线程通知机制,当一个内存共享变量被修改时,其它线程获取修改通知
- 其它方法
- 使用基于悲观锁的加锁操作
- 使用原子类变量
有序性
- 对于一个线程执行的代码一般有悖于普通顺序从上到下执行,为了提升性能,编译器和处理器通常会对指令序列进行重排序
- Java 规范规定 JVM 线程内部维持顺序化语义
- 允许指令执行顺序与代码顺序的不一致性
- 只需保证指令执行结果与代码顺序化执行结果一致即可
- 该过程称为指令重排序
- 优缺点
- JVM 能根据处理器特征(CPU 多级缓存 Cache,多核处理器),适当对机器指令进行重排序,使机器指令能更符合 CPU 的执行特征,最大限度发挥机器性能
- 指令重排可以保证串行语义一致,但不保证多线程间语义一致(可能产生脏读)
- 重排序在代码执行流程中的位置
- 串行单线程中代码顺序与重排序后指令执行结果一致
- 处理器在进行重排序时必须要考虑指令之间的数据依赖
- 多线程环境中线程交替执行,由于编译优化重排的存在,两个线程间使用的变量能发保持一致无法确定,结果存在随机性,某些情况下需要禁止指令重排序
多线程对变量的读写过程
读取过程
- JVM 运行程序的实体是线程
- 每个线程创建时 JVM 都会为其创建工作内存(又称为栈空间)
- 工作内存是每个线程的私有数据区
- Java 内存模型规定所有的变量均存储在主内存中
- 主内存是共享内存区域,所有线程均可访问
- 线程对于变量的操作须在工作内存中进行
- 首先将主内存的变量复制到工作内存中,对变量完成操作后,将变量写回主内存
- 不能直接操作主内存中的变量
- 各线程的工作内存均保存主内存的变量的副本
- 各线程间无法访问各种线程的工作内存,通过主线程传递变量
多线程先行发生原则 happens-before
在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,或是代码重排序
则两个操作之间必须存在 happens-before(先行发生原则),逻辑上的先后关系
x,y 的 case 说明
x=5 | 线程 A 执行 |
---|---|
y=x | 线程 B 执行 |
上述称之为:写后续 | y 是否等于 5 呢? |
- 如果线程 A 的操作(x=5)happens-before 线程 B 的操作(y=x),那么可以确定线程 B 执行后 y=5 必定成立
- 若不存在 heppens-before 原则,则 y=5 不一定成立
- happens-before 对程序而言,包含了可见性和有序性的约束
happens-before 原则说明
Java 规范下 JMM 的 happens-before 原则,是判断数据是否存在竞争,线程是否安全的非常重要的手段
依赖此原则,代码编写过程就无需时时处处添加 volatile 和 synchronized 保证程序的有序性;仅通过 happens-before 原则下的规则就可以解决并发环境下两个操作之间是否可能存在冲突的所有问题,无需陷入晦涩难懂的底层编译原理中
happens-before 大原则
- 一个操作 happens-before 另一个操作,第一个操作的执行结果对第二个操作可见
- 第一个操作的执行顺序在第二个操作之前
- 两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行
- 若重排序后的执行结果与 happens-before 一致,则重排序合法
- 若重排序后的执行结果与 happens-before 一致,则重排序合法
happens-before 的 8 条原则
次序规则
- 一个线程内,按照代码顺序,写在前面的操作,先行发生于写在后面的操作
- 线程内部,串行,顺序结构
锁定规则
- 一个 unLock 操作 happens-before 后面(时间上的先后)对同一个锁的 lock 操作
- 同一把锁,若存在一次 lock 则需要先释放,才能再加锁(除非可重入锁)
volatile 变量规则
- 对于一个 volatil 变量的写操作先行发生于后面对该变量的读操作,则前面的写对后面的读是可见的
- 先写后读,写的操作,对读操作可见
传递规则
- 若 A 先行发生于 B,B 先行 发生于 C,则 A 先行发生于 C
线程启动规则(Thread Start Rule)
- Thread 对象的 start()方法优先发生于线程的每一个动作
- Thread 线程的所有操作都基于是否成功执行了 start()
线程中断规则(Thread Interuption Rule)
- 对线程 interupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 先调用 interupt()(仅将标志位设置为 true),后面 cpu 看心情中断线程
- 通过 Thread.interupted()检测到是否发生中断
线程终止规则(Thread Termination Rule)
- 线程中的所有操作先行发生于对此线程的终止检测
- 通过 isAlive 等方式检测线程是否已经终止执行
对象终结规则(Finalizer)
- 一个对象的初始化完成(构造方法执行结束),先行发生于它的 finalize()方法的开始
- finalize()方法,通常目的是对象不可撤回的丢弃之前执行的清理操作
- 对象没有完成初始化之前,不能调用 finalize 方法
Summary
- 在 Java 中 Happens-Before 的语义本质上就是一种可见性
- A Happens-Before B 意味着 A 的方式过的事情对 B 而言是可见的,无论 A 与 B 两个事件是否在同一个线程中
- JMM 对 Happens-Before 的设计
- 针对程序员,设计了 happens-before8 条规则
- 针对 JVM,为了尽可能少的对编译器和处理器约束而提高性能,JMM
- 在不影响程序执行结果的前提下对其不做要求(允许指令重排序)
- 在不影响程序执行结果的前提下对其不做要求(允许指令重排序)
case
假设存在线程 A 和 B
线程 A 调用 setValue()
线程 B 调用同一个对象 getValue()
则线程 B 的到的返回值是什么?
- analysis
- 两个方法不在同一个线程中,不满足次序规则(单线程)
- 两个方法没有加锁,不满足锁定规则
- 变量没通过 volatile 修饰,不满足 volatile 变量规则
- 不满足传递规则
- 不涉及线程生命周期和对象终结规则
- anwser
由于不满足 happens-before 原则,
虽然可以确定 A 线程优先于 B 线程执行,但是无法确认 B 线程最终得到的结果
该代码是线程不安全的
- 修复方案 1
- 加锁,将 get 和 set 方法加上悲观锁
- 弊端,写读均为独占锁,读操作使得代码效率下降,适合并发量小的场景
- 修复方案 2
- 将 value 定义为 volatile 变量
- 由于 setter 方法对 value 的修改不依赖于 value 原值,满足 volatile 关键字使用场景