同步与并发
线程并发编程模型有那两个?
在我们多线程并发编程的时候,总会遇到为什么线程之间如何同步?线程之间如何通讯?
共享内存并发模型
顾名思义,共享内存模式,就是线程之间有一块共享的区域,通过读写内存*享区域的数据来隐式进行通讯
消息传递并发模型
此模式下,内存是没有共享区域来交互数据,只能相互发送消息来进行显式的数据交换
线程在模型中如何同步
Java内存模型(Java Memory Model)
- 实例,数组,对象都保存在堆内存中,堆内存在多线程之间共享,在后面我们称之它为主内存。
- 局部变量,计算过程,返回值,异常等都在栈内存,这些不受模型影响,也不会在多线程之间共享
本地内存
它并不真实存在,它涵盖了缓存,写缓冲区,寄存器及其他硬件与编译器优化。
重排
重排是为了将串行语句,根据规则重排并使用并行执行提升执行性能,最终确保满足as if serial 原则
编译器重排
CPU重排
指令并行技术:在没有数据依赖性的语句中,改变对应机器指令的重拍顺序
内存重排:CPU使用缓存与读写缓冲区,使得加载和存储操作看起来是乱序执行
处理器与上面说的内存模型相似。
内存模型是把数据读取到本地内存,然后修改后刷新回到主内存。
处理器使用写缓冲区来保存临时数据,然后刷到内存。
写缓冲区与本地内存类似,互不可见,仅对自己所在的处理器/线程可见。
例: int x = 0; int y = 0;
处理器A i = 99 , x = j
处理器B j = 1 , y = i
如果是有序执行x,y应该为 99 1
但是指令重排后有可能是 x = 0 , y = 0;
处理器步骤
1. i = 99
2. x = j
3. j = 1
4. y = i
内存步骤
2,4,1,3 读写乱序重排
如何避免
我们通过增加内存屏障的方法来禁止对特定类型的重排
数据依赖性
拥有数据依赖性的操作,不会被重排,重排规则为:写写,读写,写读相互依赖不会被重排
写写(i = 1,i =2 )
写读 ( i = 4, a =i )
读写 ( i = b, i = 2 )
as-if-serial
该语义定义了。无论如何重排,也不会影响单线程执行结果的规范。
例: int i = 9; // line 1
int j = 10; // line 2
int k = i * j // line 3
l1,l2 与 l3 存在数据依赖,所以 l3的位置不会比l1,l2高,但是l1,l2没有数据依赖,所以他们可以是重排的
单线程任务看似有序,其实也是重排的,不过as if serial 使单线程任务无需担心重排对结果的干扰
多线程重排
class A {顺序执行的话,L1 -> L2 -> L3 - > L4 最后 a = 3
boolean flag = false;
int a = 98;
public void write(){
a = 1; //L1
flag = true; // L2
}
public void read(){
if ( flag ) //L3
a += 2; //L4
}
}
但如果有2条线程Thread_A与Thread_B,A执行write,B执行read
a的结果有可能是98,首先L1与L2,L3与L4没有我们在上面数据依赖性中所说的数据依赖,那么执行顺序可能是L4-> L1 -> L2 -> L3
假设:L4先执行那么得到98+2,并保存到重排序缓存中,等L3执行的时候把98+2的结果写入i,L3与L4虽然没有数据是有控制依赖。对于控制依赖,编译器和处理器会猜测的执行来控制并行。
假设:L2先执行,随后L3 true,L4位98+2,再随后L1才执行。
从上面两种多线程重排的重排后的执行假设可以看出单线程会有as-if-serial规范保护,然而多线程则被重排序破坏了程序本身的结果和过程
Happens-Before 原则
一个操作结果对另一个操作可见,那么他们就属于happens-before关系
规则
1. 一个线程中的每个操作都happens-before它的后续操作
2. 监视器(synchronized)解锁并happens- before 于后续这个监视器的加锁
3. volatile的写,happens-before于后续对这个volatile的读
4. 传递性,如果A hb B ,B hb C, 那么 A 就一定 hb C
Happens-Before对编译器与CPU的重排定义了规范。
监视器hb关系
class A{
public synchroniezd void write(){ //L1 得到锁
a++; //L2
} //L3 释放锁
public synchroniezd void read(){ //L4得到锁
int i = a ; //L5
} //L6 释放锁
}
线程A调用write,线程B调用read
1. L1 HB L2 , L2 HB L3 / L4 HB L5 , L5 HB L6
2. 根据Hb规则第二条,监视器解锁并hb于后续这个监视器的加锁, L3 HB L4
3. 传递后 L2 HB L5
获取锁就是得到锁后再本地内存进行计算和操作,当释放锁的时候将数据刷回主内存区,从而完成与其他线程的交互
Volatile
特性1:对一个volatile变量的读,总是能看到最这个volatile变量的最后写入(happens-before)
内存屏障
屏障保证了不会被重排规则: