并发编程之顺序性
编发编程的3大特性
1、 可见性
2、顺序性
3、原子性
并发编程的顺序性概念含义
顺序指定是,程序代码是按照什么顺序执行的,比如java语言编写的代码,在被翻译成class 字节码后,加载到jvm中执行,最终执行的汇编指令的顺序。
// 这2行代码在计算机执行的时候,一定是先执行x=10,再执行x++
x=10;
x++;
//d但是下面的2行代码,一定是先执行a=5,再执行b=6的吗?
a=5;
b=6;
确定一个现况-汇编语言执行的不是按照我们书写的程序执行这样。
1、java 语言如何模拟
2、指令重排序
汇编指令,不是按照书写的顺序执行的,比如"a=5;b=6;"这2行指令执行顺序是可以调整的。
3、为什么要有执行令重排序
为了提高效率。
ps : 寄存器的效率是内存的100倍。
as-if-serial
1、什么样的场景下,执行时可以排序的,什么样的场景下执行时不可以重新排序的?
回答这个问题是这样的,在不影响单个线程的最终一致情况下,单个线程内的指令是可以调整顺序的。简单理解就是指令间不存在依赖关系。这样的场景下,执行时可以重新排序的。
反之,指令间有依赖关系的,有先后顺序要求的,则是不能重新排序的。
理论上,两条指令互不影响,不影响单线程的最终一致性,那么这两条指令是可以互换顺序。但是,但是,但是,java不是这样的。
2、推荐一本并发编程的书
《java并发编程实战》
关于并发编程的诡异问题分析
1、第一个问题
程序代码如下图:
上面的程序有2个问题
1) 可见性问题,不能马上输出 number的值
ready 被设置成true,并不能马上停止的线程会发生。【这个不是定义的,有这样概率】
mesi的主动性和 Thread.yield()的同步刷新都会让程序能执行输出 。
把number的修饰加上 volatile ,能保证number变量数据的修改,对其他线程可见。
2) 顺序性问题, number 输出是0 情况会出现
number = 42 ; 和 ready = true ; 这2行代码执行的顺序,不一定是按照书写的顺序,可能会出现指令重排序问题。
2、第二个问题
ps: 实际的输出结果,大多数是8,也会有输出0的情况。为了方便输出,在System.out.println(num);地方修改成System.out.println(thid.num);
补充知识
对象的创建过程
Object o = new Object(); 这行程序的字节码指令(汇编指令)是分多步骤执行的,先执行 new指令,再执行dump执行,再init,再执行关联操作。整个的过程中,会有一个半成品对象的状态。理解有这样一个过程即可。(更多的字节码指令解析看详细的对象创建过程)
指令可能会换顺序。此时num=0,还没有赋值为8 ,此时线程启动了,取到的是中间状态。
this的溢出问题,this溢出了构造方法。
所以,不要在构造函数中new线程并启动。构造函数中可以new线程对象,但是不要在里面启动start,在单独的方法里启动,这样不会出现上面的问题。看下面代码。
volatile 的禁止指令重排序语义。
DCL 单例中 instance 要不要volatile 修饰?
synchronized 保证了instance 具备原子性,但是没有保证指令的重排序性。
volatile 语义保证了,instance 具备指令不重排序的语义。
volatile 底层的实现原理
1、 CPU的内存屏障
内存屏障 memory barrier,能够让CPU或者编译器在内存访问上有序。
1.1 CPU内存的几种内存屏障
1) 通用的内存屏障,保障读写有序
2) 写操作内存屏障,保障写有序
3)读操作内存屏障,保障读有序
1.2 store buffer
1.3 store buffer 的弊端
数据不一致性
1.4 store forwarding
store forwarding 构件是解决 store buffer 的数据不一致性问题(只是缓解了 store buffer 的问题)。
1.5 memory barrier
memory barrier ,保障缓存中的数据要和内存中的数据一致。
写屏障,强制写到内存中;
读屏障,强制从内存中读取;
内存屏障,结合高效缓存机制,以及缓存一致性协议,完成对指令的执行,控制它是否能采用重排序。
invalidate queue
1.6 arm架构的内存屏障
1.7 x86架构的内存屏障
2、JSR的内存屏障
3、jvm中volatile实现
4、happen-before 原则
5、as-if-serial 执行重排序,不会影响单线程的最终一致性。