Java内存模型
Java内存模型(Java Memory Model)是解决Java的工作内存和物理内存的关系,特别在并发情况下怎么保障多个线程对物理内存的访问不冲突。需要与JVM内存管理机制区别开。
主内存和工作内存
JMM主要是定义程序中各个变量的访问规则,也就是变量从JVM内存的存取实现。JMM规定所有变量都存储在 *主内存(Main Memory)*中,而每个线程都有自己的 工作内存(Working Memory),工作内存中保存了主内存中变量的副本,线程对变量的操作必须通过工作内存来进行。
由于工作内存是线程私有,当存在多个线程同时访问一个变量时,需要通过主内存来同步。
提示
这里说的变量不包括线程私有变量,例如局部变量和方法参数等,线程私有变量不会被共享,不存在竞争问题。
内存的操作
对于工作内存和主内存的变量应如何交互,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变量的值能立即同步到主内存。
注意
但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