Java内存模型 与 多线程

本文最后更新于:2022年7月19日 下午

概览:Java内存模型

预警!仅用于本人快速自学,不过欢迎指正。

JMM - Java Memory Model

为什么出现?

java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。

JMM规定:

  • 所有的变量都存储在主内存中,包括实例变量和静态变量,不包括局部变量和方法参数。
  • 每个线程都有自己的工作内存,工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。
  • 每个线程的工作内存都是独立的,线程操作数据只能在工作内存中进行,然后刷回到主存
  • JMM围绕着三大特征建立起来:原子性、可见性以及有序性。

容易误解的概念,内存结构, —— JVM内存结构(堆、栈、垃圾回收等)

并发三大特性:原子性、可见性以及有序性

  • synchronized能保证三大特性
  • volatile能保证可见性以及禁止指令重排

原子性

一个操作不可分割,一个线程在执行过程中不会被其他线程干扰。

eg:int i = 10; i++; 对应的字节码

1
2
3
bipush        10
istore_1
iinc 1, 1
  • istore_1 表示把bipush放入到操作数栈的10放入槽位1
  • iinc:表示对于1号槽位的数据做加法,加1,这个操作是直接在槽位上完成的。

而对于静态变量 i++;

1
2
3
4
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 加法
putstatic i // 将修改后的值存入静态变量i

要保证这样一个指令的原子性,需要使用synchronized来保证

包围的代码块 —— monitorenter 和 monitorexit

可见性

例子:一个boolean run = true;开一个线程不断判断run,执行。

主线程修改run为false,但是线程没有停止。

原因:JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率。

可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。

Java是利用volatile关键字来提供可见性的。

当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值.

除了volatile关键字之外,final和synchronized也能实现可见性。

synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中。

final修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。

有序性

指令重排现象:JIT编译器在运行时的一些优化,比较少见。

volatile 修饰的变量,可以禁用指令重排

  • JVM 会在不影响正确性的前提下,可以调整语句的执行顺序。
  • 单例模式的 double-check-lock防止指令重排,使用volatile

INSTANCE = new Singleton() 对应的字节码,指令重排可能导致 4,7位置互换,从而导致错误

1
2
3
4
0: new #2 // class cn/itcast/jvm/t4/Singleton 生成引用地址
3: dup // 复制一份
4: invokespecial #3 // Method "<init>":()V
7: putstatic #4 // Field

happens-before规则

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见‘
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

JMM 八种内存交互操作

  • lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。

  • read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。

  • load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。

  • use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

  • assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。

  • store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。

  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM对8种内存交互操作制定的规则吧:

  • 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。

  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。

  • 不允许线程将没有assign的数据从工作内存同步到主内存。

  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。

  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。

  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。

  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。

  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。


CAS - 乐观锁机制

compare and set

使用Unsafe来直接操作内存

三个参数,内存地址,期望原值,修改值。

参考链接:面试官问我什么是JMM_资源分享_牛客网 (nowcoder.com)


Java对象头

在HotSpot虚拟机中,对象在内存中的布局分为三部分:

  • 对象头:包含类型指针和MarkWord,如果是数组对象,还存储数组长度。
    • MarkWord:普通情况下存储对象自身的运行时数据,hashCode,GC分代年龄
  • 实例数据
  • 对齐填充

轻量级锁

目的:在没有多线程竞争的前提下,减少传统重量级锁产生的性能消耗。

多线程下的synchronized就是对同一个对象的对象头中的MarkWord中的变量进行CAS操作。

工作流程:

  1. 在进入代码同步块的时候,如果同步对象没有被锁定,则在当前线程的栈帧中建立一个锁记录的空间,存储MarkWord的拷贝结果。
  2. 使用CAS操作尝试把MarkWord更新为指向锁记录的指针,如果更新成功,就表示线程拥有了锁,并更改锁标志位。
  3. 如果更新失败,则意味着有其他线程在竞争,如果出现两条以上的线程争用同一把锁,那么锁就要膨胀为重量级锁,同时更改锁标志。此时MarkWord中存储的就是执行重量级锁的指针。后面等待的线程也必须进入阻塞状态。

加锁使用CAS,解锁也使用CAS,如果解锁CAS失败,说明有其他线程尝试获取该锁,那么就要在释放锁的同时,唤醒被挂起的线程。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!