为了提高计算能力,减少计算机存储设备与处理器运算速度的差距,现代计算机系统在内存
与处理器
之间加入了一层读写速度与处理器运算速度尽可能接近的高速缓存(Cache)
作为内存与处理器之间的缓冲.
将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存中,这样处理器就无须等待缓慢的内存读写.
- 这种模型虽然很好的解决了处理器与内存的速度矛盾,但却引入了
缓存一致性(Cahce Coherence)
的问题:
多处理器系统中,每个处理器都有自己的高速缓存,它们共享同一块主存(Main Memory)
.当多个处理器的运算都涉及同一块主内存区域时,可能导致各自的缓存数据不一致.
- 为了解决一致性问题,需要各个处理器访问缓存时都遵循
一致性协议
,读写时根据协议进行操作
为了使处理器内部单元能被充分利用,处理器可能会对代码进行乱序执行(Out-Out-Order Execution)优化
,处理器会在计算之后将乱序执行的结果重组,保证结果与顺序执行结果一致,但并不保证与输入代码的顺序一致.
- Java虚拟机的即时编译器中也有类似的
指令重排序(Instruction Reorder)
优化.
-
Java规定了所有的变量(
静态字段
,实例字段
和构成数组对象的元素
)都存储在主内存
中. -
每条线程拥有自己的
工作内存
,其中保存了该线程使用到的主内存副本拷贝
. -
线程对变量的所有操作(赋值,读取等)都必须在自己的工作内存中进行,而不能直接读写主内存中的变量.不同的线程之间也无法直接访问对方工作内存中的变量.
线程间的变量值传递均需要通过主内存来完成.
-
主内存
主要对应于Java堆
中的对象实例
数据部分,工作内存
则对应于虚拟机栈
中的部分区域.从更低的层次上说,
主内存
直接对应于物理硬件的内存
,而为了获取更好的运行速度,虚拟机(甚至是硬件系统本身的优化措施)可能会让工作内存
优先存储于寄存器
和高速缓存
中.
-
Java内存模型定义了8种操作(原子,不可再分)来完成主内存与工作内存之间的具体交互细节:
-
lock(锁定): 作用于
主内存变量
,把一个变量标识为一条线程独占状态. -
unlock(解锁): 作用于
主内存变量
,把处于锁状态的变量释放出来. -
read(读取): 作用于
主内存变量
,把变量值从主内存
传到线程的工作内存
,以便load
动作使用. -
load(载入): 作用于
工作内存变量
,把read
操作从主内存
得到的变量放入工作内存
的变量副本
中. -
use(使用): 作用于
工作内存变量
,把变量值传递给执行引擎
.当虚拟机遇到需要使用变量值的字节码指令时会执行这个操作. -
assign(赋值): 作用于
工作内存变量
,把执行引擎
的值,赋给工作内存变量
.虚拟机遇到赋值的字节码指令执行此操作. -
store(储存): 作用于
工作内存变量
,把工作内存变量
的值传送到主内存
中. -
write(写入): 作用于
工作内存变量
,把store
操作从工作内存
得到的变量的值放入主内存变量
中.
-
- 把变量从
主内存
复制到工作内存
: 顺序执行read
和load
. - 把变量从
工作内存
同步回主内存
: 顺序执行store
和write
.
(顺序执行,不保证连续执行,可插入其他指令)
-
Java内存模型规定执行上必须满足以下规则:
-
不允许
read
和load
,store
和write
操作之一单独出现. -
不允许一个线程丢弃它的
assign
操作. -
不允许线程无原因的(未发送
assign
)把数据从工作内存
同步回主内存
中. -
新的变量只能在
主内存
中诞生. -
一个变量同一时刻只允许一个线程对其进行
lock
操作,lock
可被同个线程重复执行. -
对一个变量进行
lock
操作,工作内存
会清空此变量值,执行引擎
使用这个变量之前,需重新执行load
(主内存到工作内存)或者assign
(执行引擎到工作内存)初始化变量值. -
不允许
unlock
被其他线程lock
或未被lock
的变量. -
对一个变量
unlock
之前,必须先同步(执行store
,write
)回主内存
中.
-
-
volatile的特殊规则(有空另开一篇详细整理下):
volatile是Java虚拟机提供的最轻量级同步机制.
-
两种特性:
-
可见性: 保证volatile修饰的变量对所有线程可见.
-
volatile变量只能保证可见性,只能再一下场景使用:
-
运算结果不依赖当前值,或者能保证只有单一线程修改变量值.
-
变量不需要与其他的状态变量共同参与不变约束.
-
-
-
禁止重排序优化: 普通变量仅仅会保证该线程方法的执行过程中所依赖的赋值结果的地方都能获得正确结果.
volatile
能保证变量赋值操作顺序与代码中的执行一致.- 内存屏障(Memory Barrier/Memory Fence): 指
重排序
时不能把后面的指令重排序到内存屏障之前的位置.
- 内存屏障(Memory Barrier/Memory Fence): 指
-
-
volatile变量读操作性能消耗与普通变量几乎没差别,写操作开销比较大.
-
假设T标识一个线程,V和W分别表示两个volatile型变量,在进行
read
,load
,use
,assign
,store
和write
时需要满足如下规则:-
每次使用V前都必须从
主内存
刷新最新的值,保证能看见其他线程对变量V的最后修改. -
工作内存
中,每次修改V后都必须立刻同步回主内存中,保证其他线程可以看到自己对变量V的修改. -
要求volatile修饰的变量不会被指令重排序优化,保证代码
执行顺序
与程序的顺序
相同. -
对于long,double型变量的特殊规则:
- Java内存模型允许虚拟机将没被volatile修饰的64位数据(long和double)的读写操作划分为两次32位操作.即允许虚拟机不保证64位数据的
load
,read
,store
和write
的原子性.
- Java内存模型允许虚拟机将没被volatile修饰的64位数据(long和double)的读写操作划分为两次32位操作.即允许虚拟机不保证64位数据的
-
-
原子性(Atomicity):
-
基本数据类型(long和double除外)的读写是具备原子性的.
-
更大范围的原子性保证:
synchronized
关键字,Java虚拟机提供monitorenter
和monitorexit
式使用lock
和unlock
操作.
-
-
可见性(Visibility):
-
volatile
保证了多线程操作时变量的可见性. -
sychronized
可见性:对一个变量进行unlock
之前必须把此变量同步回主内存中. -
final
可见性: 被final
修饰的字段在构造器中一旦初始化完成,且构造器没有把this
引用传递出去,其他线程中就能看见final
字段的值.
-
-
有序性(Ordering):
-
Java提供
volatile
和synchronized
保证线程之间操作的有序性:-
volatile
本身包含禁止指令重排序的语义. -
synchronized
变量在一个时刻只允许一条线程对其进行lock
操作,决定同一个锁的两个代码块只能串行进入.
-
-
先行发生原则: 判断数据是否存在竞争,线程是否安全的主要依据.如果操作A先行发生于操作B,在发生B操作之前,操作A产生的影响能被操作B观察到.
-
Java内存模型的先行发生关系:
- 程序次序规则:
在一个线内,按照代码顺序,书写在前面的操作先行发生于写在后面的操作(注意区分
先行发生
与先发生
).- 管程锁定规则:
一个
unlock
操作先行发生于后面对同一个锁的lock
操作.- volatile变量规则:
对一个
volatile
变量的写操作先行发生于后面对这个变量的读操作.- 线程启动规则:
Thread
对象的start()方法先行发生于此线程的每个动作.- 线程终止规则:
线程中的所有操作都先行发生于对此线程的终止检测.
- 线程中断规则:
对线程
interrupt()
方法的调用先行发生于被中断线程的代码检测中断事件发生.- 对象终结规则:
一个对象的初始化完成(构造函数执行结束),先行发生于它的
finalize()
方法的开始.- 传递性:
操作A先行发生于操作B,操作B先行发生于操作C,则操作A先行发生于操作C.