深刻浅出 Java Concurrency (4): 原子操做 part 3 指令重排序与happens-before法则


在这个小结里面重点讨论原子操做的原理和设计思想。html

因为在下一个章节中会谈到锁机制,所以此小节中会适当引入锁的概念。java

在Java Concurrency in Practice中是这样定义线程安全的:缓存

当多个线程访问一个类时,若是不用考虑这些线程在运行时环境下的调度和交替运行,而且不须要额外的同步及在调用方代码没必要作其余的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的。安全

显然只有资源竞争时才会致使线程不安全,所以无状态对象永远是线程安全的。架构

原子操做的描述是: 多个线程执行一个操做时,其中任何一个线程要么彻底执行完此操做,要么没有执行此操做的任何步骤,那么这个操做就是原子的。并发

枯燥的定义介绍完了,下面说更枯燥的理论知识。app

指令重排序函数

Java语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程经过叫作指令的重排序。指令重排序存在的意义在于:JVM可以根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的从新排序机器指令,使机器指令更符合CPU的执行特色,最大限度的发挥机器的性能。性能

程序执行最简单的模型是按照指令出现的顺序执行,这样就与执行指令的CPU无关,最大限度的保证了指令的可移植性。这个模型的专业术语叫作顺序化一致性模型可是现代计算机体系和处理器架构都不保证这一点(由于人为的指定并不能老是保证符合CPU处理的特性)。atom

咱们来看最经典的一个案例。

package xylz.study.concurrency.atomic;

public class ReorderingDemo {

static int x = 0, y = 0, a = 0, b = 0;

public static void main(String[] args) throws Exception {

for (int i = 0; i < 100; i++) {
x=y=a=b=0;
Thread one = new Thread() {
public void run() {
a = 1;
x = b;
}
};
Thread two = new Thread() {
public void run() {
b = 1;
y = a;
}
};
one.start();
two.start();
one.join();
two.join();
System.out.println(x + " " + y);
}
}

}

 

在这个例子中one/two两个线程修改区x,y,a,b四个变量,在执行100次的状况下,可能获得(0 1)或者(1 0)或者(1 1)。事实上按照JVM的规范以及CPU的特性有极可能获得(0 0)。固然上面的代码你们不必定能获得(0 0),由于run()里面的操做过于简单,可能比启动一个线程花费的时间还少,所以上面的例子难以出现(0,0)。可是在现代CPU和JVM上确实是存在的。因为run()里面的动做对于结果是无关的,所以里面的指令可能发生指令重排序,即便是按照程序的顺序执行,数据变化刷新到主存也是须要时间的。假定是按照a=1;x=b;b=1;y=a;执行的,x=0是比较正常的,虽然a=1在y=a以前执行的,可是因为线程one执行a=1完成后尚未来得及将数据1写回主存(这时候数据是在线程one的堆栈里面的),线程two从主存中拿到的数据a可能仍然是0(显然是一个过时数据,可是是有可能的),这样就发生了数据错误。
在两个线程交替执行的状况下数据的结果就不肯定了,在机器压力大,多核CPU并发执行的状况下,数据的结果就更加不肯定了。

Happens-before法则

Java存储模型有一个happens-before原则,就是若是动做B要看到动做A的执行结果(不管A/B是否在同一个线程里面执行),那么A/B就须要知足happens-before关系。

在介绍happens-before法则以前介绍一个概念:JMM动做(Java Memeory Model Action),Java存储模型动做。一个动做(Action)包括:变量的读写、监视器加锁和释放锁、线程的start()和join()。后面还会提到锁的的。

happens-before完整规则:

(1)同一个线程中的每一个Action都happens-before于出如今其后的任何一个Action。

(2)对一个监视器的解锁happens-before于每个后续对同一个监视器的加锁。

(3)对volatile字段的写入操做happens-before于每个后续的同一个字段的读操做。

(4)Thread.start()的调用会happens-before于启动线程里面的动做。

(5)Thread中的全部动做都happens-before于其余线程检查到此线程结束或者Thread.join()中返回或者Thread.isAlive()==false。

(6)一个线程A调用另外一个另外一个线程B的interrupt()都happens-before于线程A发现B被A中断(B抛出异常或者A检测到B的isInterrupted()或者interrupted())。

(7)一个对象构造函数的结束happens-before与该对象的finalizer的开始

(8)若是A动做happens-before于B动做,而B动做happens-before与C动做,那么A动做happens-before于C动做。

volatile语义

到目前为止,咱们屡次提到volatile,可是却仍然没有理解volatile的语义。

volatile至关于synchronized的弱实现,也就是说volatile实现了相似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其余的线程。

volatile包含如下语义:

(1)Java 存储模型不会对valatile指令的操做进行重排序:这个保证对volatile变量的操做时按照指令的出现顺序执行的。

(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其余对CPU不可见的地方,每次老是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程老是可见的,而且不是使用本身线程栈内部的变量也就是在happens-before法则中,对一个valatile变量的写操做后,其后的任何读操做理解可见此写操做的结果。

尽管volatile变量的特性不错,可是volatile并不能保证线程安全的,也就是说volatile字段的操做不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程可以理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

volatile一般在下面的场景:

 

volatile boolean done = false;

while( ! done ){
dosomething();
}

应用volatile变量的三个原则:

(1)写入变量不依赖此变量的值,或者只有一个线程修改此变量

(2)变量的状态不须要与其它变量共同参与不变约束

(3)访问变量不须要加锁

 

这一节理论知识比较多,可是这是很面不少章节的基础,在后面的章节中会屡次提到这些特性。

本小节中仍是没有谈到原子操做的原理和思想,在下一节中将根据上面的一些知识来介绍原子操做。

 

参考资料:

(1)Java Concurrency in Practice

(2)正确使用 Volatile 变量

相关文章
相关标签/搜索