Java内存模型

Louis2020-12-20Java技术Java

Java内存模型(Java Memory Model)是解决Java的工作内存和物理内存的关系,特别在并发情况下怎么保障多个线程对物理内存的访问不冲突。需要与JVM内存管理机制区别开。

主内存和工作内存

JMM主要是定义程序中各个变量的访问规则,也就是变量从JVM内存的存取实现。JMM规定所有变量都存储在 *主内存(Main Memory)*中,而每个线程都有自己的 工作内存(Working Memory),工作内存中保存了主内存中变量的副本,线程对变量的操作必须通过工作内存来进行。 由于工作内存是线程私有,当存在多个线程同时访问一个变量时,需要通过主内存来同步。

TIP

这里说的变量不包括线程私有变量,例如局部变量和方法参数等,线程私有变量不会被共享,不存在竞争问题。

内存的操作

对于工作内存和主内存的变量应如何交互,JMM定义了8种原子操作。

其中四种对应于主内存:

  • lock: 把变量锁定,该变量设置为线程独占,一个变量只能被一个线程锁定
  • unlock: 解锁变量
  • read: 把变量值从主内存读出
  • write: 把变量值写入主内存

四种操作作用于工作内存:

  • load: 把read操作读到的变量放入工作内存副本中
  • store: 把工作内存的变量传入主内存中,以便于接下来的write操作
  • use: 把工作内存中的变量值传递给执行引擎,相当于从工作内存中读取变量值
  • assign: 把从执行引擎接收到的值传递给工作内存中的变量,相当于给工作内存中的变量赋值

以上8种操作必须满足以下规则:

  • read之后必须load,store之后必须write,但并不要求read和load必须连续执行,二者之间还可以有其他操作
  • 所有assign操作都不能被丢弃,必须同步回主内存
  • 如果没有发生过assign操作,则禁止将值同步回主内存
  • lock操作可以被一个线程执行多次,必须执行相同次数的unlock才能解锁变量
  • lock会清空工作内存中的变量值
  • unlock之前必须先lock,必须执行store和write把变量值写回主内存

总结为:

  • 8个操作的大体顺序是lock-read-load-use/assign-store-write-unlock
  • 基本上read/load/use、assign/store/write这几个组合一般都是一起出现的,如果不需要太关注细节又便于记忆,可以把8个操作归类为加锁、读取、写入、解锁
  • 对变量的读写不一定要lock,除非需要线程独占申请排他锁
  • 变量load之后不一定要assign,也就是可以只读取变量而不修改

volatile变量

volatile变量能够保证线程对其的“可见性”,也就是说线程A对变量p的修改可以被线程B立即读到。 其实现是通过将read、load、use这3个操作绑定,read后必须紧接着load然后紧接着use,以保证每次读取volatile变量都是从主内存中刷新的最新值; 同样assign、store、write这3个操作也要绑定,保证每次修改volatile变量的值能立即同步到主内存。

WARNING

但volatile并不能保证线程安全,例如对volatile变量运行a++语句,需要先use a然后在assign a,但use和assign两个操作并不是原子操作。 反过来说,如果对变量的修改不依赖于当前值则可以使用volatile实现线程安全同步。

另外,volatile能够避免指令重排。例如对如下代码:

volatile boolean initial = false;
public void init() {
	// do something
	initial = true;
}

防止指令重排的意义:在多核CPU中,由于底层的指令重排优化,有可能initial=true提前执行,如果另一个线程恰好读取了这个状态,会导致程序未正常初始化。

原子性、可见性和有序性

JMM中提到并发操作最主要涉及这几个特性。

  • 原子性:上述提到的8个操作本身都是原子操作;对于基本类型的访问读写也可以认为是原子的,但像a++这种除外
  • 可见性:是指当一个线程修改了共享变量的值,其他线程能够立即获得这个修改。
  • 有序性:在线程内观察,本线程的所有操作都是有序的,执行器能保证语句执行在线程内表现为串行的语义,但在一个线程观察另一个线程,由于指令重排和工作内存/主内存的同步延迟,可能会发现另一个线程的操作是无序的。

具体到语法中: volatile 保证了可见性和有序性,上面已经提到; synchronized 保证了原子性、可见性、有序性,其实现是通过给变量读取前加lock操作来实现的,并且最多只能有一个线程获得lock; 容易忽略的是final 也保证了可见性,一旦构造器初始化完成,final字段的值就是不变且确定的,保证了可见性。

先行发生原则

在多线程环境下,写在前面的代码不一定在时间上就先执行;其实在单线程内也不一定能保证,因为有指令重排优化,但单线程可以保证先行发生。

先行发生(happens-before)原则,是指两个操作A和B,如果我们说A先行发生于B,就是说操作A产生的影响能够被操作B感知到。

先行发生并不等同于时间上先执行,例如线程a先修改了某个值,还未同步到主内存,线程b此时有可能还读不到新值,这就不满足先行发生原则。

在Java中有以下几个先行发生原则: // TODO

Last Updated 2024/11/24 20:50:53