这部分内容,跟并发有关
我们知道,多任务处理,在现代操作系统几乎是必备功能。让计算机同时去做几件事情,不仅因为CPU运算能力太强大了,还有一个重要原因,CPU的运算速度远远高于它的存储和通信子系统的速度,大量时间耗费在磁盘I/O,网络I/O,数据库访问
虚拟机层面,如何实现多线程,多线程之间因数据共享或竞争而引发的一系列问题及解决方案
物理机遇到的并发与虚拟机中的情况,有不少相似之处,再扩展到分布式系统,我发现,其实也有不少相似之处。这之间有许多值得玩味的地方。
让计算机并发执行多个运算任务
这里面,不可能仅仅靠CPU计算就搞定的。CPU至少要跟内存交互,读取运算数据,存储运算结果,这个IO很难消除。当然,也无法仅仅靠CPU内的寄存器完成所有运算任务
CPU与存储设备之间的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写尽可能接近CPU速度的高速缓存(Cache),作为内存与CPU间的缓冲。将运算需要的数据复制到缓冲,让运算快速进行,完后将缓存同步到内存。如此,CPU就无需等待缓慢的内存读写
在速度差距很大时,利用缓存来缓冲,用空间换时间;但同时会带来数据同步问题
引入了缓存一致性(Cache Coherence)问题
多核处理器里,每个CPU都有自己的高速缓存(一级、二级、三级),而它们又共享同一主内存。
当多个CPU的运算任务都涉及同一块主内存区域,可能导致各自的缓存数据不一致;数据同步回主存时,以谁的缓存数据为准呢?
为解决一致性问题,需要CPU访问缓存时都遵循一些协议,读写时,根据操作协议来。如MSI、MESI、MOSI、Synapse、Firefly、Dragon、Protocol
内存模型: 可以理解为,在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象
同时为了使得处理器充分被利用,CPU可能会对输入代码进行乱序执行(Out-of-Order Execution)优化,CPU会在计算后将结果重组,保证结果与顺序执行一致。Java虚拟机的即时编译器也有类似的指令重排序(Instruction Reorder)优化
若一个计算任务依赖另一计算任务的中间结果,那其顺序性,不能靠代码的先后顺序来保证
Java虚拟机使用定义种Java内存模型,以屏蔽各种硬件和OS的内存访问差异,以实现让Java程序在各个平台下都能达到一致的并发效果。C/C++直接使用物理硬件和OS的内存模型。
目标
定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题
工作内存
主內存
一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存,Java内存模型定义了8种操作。这些操作,都是原子操作
Operation | Place | Instruction |
---|---|---|
lock | Main Memory | 将变量标识为一条线程独占状态 |
unlock | Main Memory | 释放被锁定的变量,释放后的变量才能被其他线程锁定 |
read | Main Memory | 变量值从主内存读取到线程的工作内存,以便紧接着的load操作 |
load | Working Memeory | 将read操作得到变量值放入工作内存的变量副本中 |
use | Working Memeory | 将工作内存的变量值传递给线程执行引擎 |
assign | Working Memory | 將一个从执行引擎接收到的值,赋给工作内存的变量 |
store | Working Memory | 将工作内存中的一个变量的值,传送到主内存,以便紧接着的write操作 |
write | Main Memory | 将store操作的变量值,放入主内存的变量中 |
变量从主内存复制到工作内存,顺序执行read和load
变量从工作内存同步到主内存,顺序执行store和write
Nonatomic Treatment of double and long Variables
Java内存模型要求8个操作都具有原子性,但对64位的数据类型,long和double,模型定义了相对宽松
允许,并强烈建议,虚拟机将这些操作实现为原子性操作。
目前商用Java虚拟机几乎都选择把64位数据的读写作为原子操作来对待
编写代码时,一般不需为long或double专门声明为volatile
前面说过,Java内存模型,其实是定义读写内存变量的规则。
有些类型的变量比较特殊,除了上面所述的8个基本操作原则外,有特殊的规则。
特殊规则
特殊语义
volatile是轻量级的synchronized,在多CPU开发中,保证了共享变量的“可见性”。
指当一条线程修改了变量的值,新的值可以被其他线程立即知道
volatile只能保证可见性,但无法保证原子性,which is a necessity for synchronization.
因此,如果不符合下面两个规则的运算场景,我们需要通過加锁,如synchronized关键字和java.util.concurrent包下的原子类,来保证源自性。如果符合,volatile就能保证同步
如下面的代码,就非常适合用volatile变量来控制并发
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true;
}
//当shutdown()被调用时,能保证所有线程中执行的doWork()方法都停下来
public void doWork() {
while(!shutdownRequest) {
//do something
}
}
被volatile修饰的变量,多执行了lock addl $0x0,(%esp)
操作
这个操作,相当于一个内存屏障(Memory Barrier/Memory Fence),意思是,重排序时,不能把后面的指令重排序到內存屏障之前的位置
lock addl $0x0,(%esp)
汇编指令,把ESP寄存器的值加0,这个是空操作。其作用,是使得本CPU的Cache写入内存,该写入动作,也会引起别的CPU或别的内核无效化(Invalidate)其Cache,相当于对Cache中的变量,做了一次如Java内存模型中的”Store且Write操作”。所以,通过这样一个空操作,可让volatile变量的修改,对其他CPU立即可见
硬件架构上讲,指令重排序,是指CPU采用了允许将多条指令不按程序规定的顺序,分开发送给各个相应电路单元处理,同时保证结果正确,与程序顺序执行的结果一致。
Java内存模型,围绕着并发过程中如何实现原子性、可见性和有序性,3个特征来建立。我们来看看哪些操作,实现了这些特征
原子性(Atomicity)
可见性(Visibility)
有序性(Ordering)
如果Java内存模型中所有的有序性,仅仅靠volatile和synchronized来完成,那么一些操作会很繁琐,但我们没有感觉得到,因为有happens-before原則。
该原则是判断数据是否存在竞争、线程是否安全的主要依据
//线程A中执行
i = 1;
//线程B中执行
j = i;
//线程C中执行
i = 2;
如果操作A和操作C之间,不存在先行发生关系,C出现在A和B之间,那么,C线程对变量j的修改,B线程不一定观察得到,此时,B读取到的数据可能不是最新的,不是线程安全的
Java内存模型中的先行发生
8条规则
传递性(Transitivity)
时间上的先后,不等于“先行发生”。
时间先后顺序与happens-before基本没太大关系,衡量并发安全问题,一切以happens-before原则为准,不要受到时间顺序的干扰
推荐阅读
infoq 深入理解Java内存模型
infoq Java并发编程的艺术
并发编程网