跳至主要內容

Java内存模型

pptg大约 4 分钟

1. 多线程安全

1.1 为什么会导致线程不安全

  1. 可见性
    • 描述:线程对共享变量修改,其他线程应立刻可见
    • 问题:CPU1在执行时先将共享变量加载到自己的高速缓存中,此时CPU2的修改对于CPU1不可见
    • 解决volatilesynchronized和各种Lock
  2. 原子性
    • 描述:操作要么全部执行成功,要么全部失败
    • 问题:由CPU分时复用引起。比如线程1在执行完之后,还没来得及将CPU缓存写入内存就被CPU2执行
    • 解决synchronized和各种Lock
  3. 有序性
    • 描述:程序按代码的先后顺序执行
    • 问题:编译程序为了优化可能会指令重排(Instruction Reorder),Java代码到执行过程会经过编译器优化重排序、指令级并行重排序、内存系统重排序三个过程
    • 解决volatile可以禁止指令进行重排序优化

1.2 Java解决并发安全的方案

Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法,并提供了 volatilesynchronizedfinal 三个关键字来禁用缓存和编译优化,并提供了Happens-Before 规则来解决并发安全问题

2. Java内存模型

2.1 介绍

Java 线程之间的通信由 Java 内存模型(JMM) 控制。JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读 / 写共享变量的副本

  • 主内存:所有线程创建的实例对象都存放在主内存
  • 本地内存:每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。每个线程只能操作自己本地内存中的变量,无法直接访问其他线程的本地内存。如果线程间需要通信,必须通过主内存来进行。本地内存是 JMM 抽象出来的一个概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
Java内存模型
Java内存模型

如图,线程AB想要通信就必须由线程A将变量同步到主内存,线程B再从主内存拿到这个变量。JMM为主内存和本地内存的同步定义了8种同步操作

  • 锁定(lock): 作用于主内存,将变量标记为一个线程独享变量
  • 解锁(unlock): 作用于主内存,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定
  • 读取(read):作用于主内存,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用
  • 载入(load):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中
  • 使用(use):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令
  • 赋值(assign):作用于工作内存,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
  • 存储(store):作用于工作内存,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用
  • 写入(write):作用于主内存,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中

2.2 Happens-Before原则

在分布式环境中,通过一系列规则来定义逻辑时钟的变化,从而能通过逻辑时钟来对分布式系统中的事件的先后顺序进行判断。逻辑时钟并不度量时间本身,仅区分事件发生的前后顺序,其本质就是定义了一种 Happens-Before 关系

Happens-Before为了尽可能减少对编译器和处理器的限制,允许不改变程序结果的重排序,而禁止改变结果的重排序

对于这段代码:

int userNum = getUserNum();   // 1
int teacherNum = getTeacherNum();   // 2
int totalNum = userNum + teacherNum;  // 3

对于1和2,显然不会改变程序的执行结果,JMM允许对其进行重排序;但3需要1和2的执行结果,也就是1,2happens-before3,所以这里不允许重排序