内存模型抽象结构
线程间协作通信可以类比人与人之间的协作的方式,在现实生活中,之前网上有个流行语“你妈喊你回家吃饭了”,就以这个生活场景为例,小明在外面玩耍,小明妈妈在家里做饭,做晚饭后准备叫小明回家吃饭,那么就存在两种方式:
小明妈妈要去上班了十分紧急这个时候手机又没有电了,于是就在桌子上贴了一张纸条“饭做好了,放在…”小明回家后看到纸条如愿吃到妈妈做的饭菜,那么,如果将小明妈妈和小明作为两个线程,那么这张纸条就是这两个线程间通信的共享变量 ,通过读写共享变量实现两个线程间协作;
还有一种方式就是,妈妈的手机还有电,妈妈在赶去坐公交的路上给小明打了个电话,这种方式就是通知机制来完成协作。同样,可以引申到线程间通信机制。
通过上面这个例子,应该有些认识。在并发编程中主要需要解决两个问题:1. 线程之间如何通信;2.线程之间如何完成同步(这里的线程指的是并发执行的活动实体)。通信是指线程之间以何种机制来交换信息,主要有两种:共享内存和消息传递。这里,可以分别类比上面的两个举例。java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量来完成隐式通信。
哪些是共享变量
java中所有的实例域、静态域和数组元素都是放在堆内存中(所有线程均可访问,是共享的)。而局部遍历,方法定义参数和异常处理参数不会再线程间共享(栈私有)
JMM抽象结构模型
因为CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每个CPU都会有缓存。因此共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把主存中的共享变量拷贝到自己的工作内中,并在某个时刻将工作内存的变量副本写回到主存中。
由此线程A要与线程B完成通信的话要经过如下操作
1.线程A从主存中拷贝共享变量进行操作,之后将数据重新写入到主存
2.线程B从主存获取最新的共享变量值
重排序
在不改变程序执行结果的前提下,尽可能的提高并行度。因此为了提高性能,编译器和处理器常常会对指令进行重排序
一般重排序分为以下三种
- 编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
- 指令级并行的重排序。现在处理器采用了指令级并行技术来将多条指令折叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
- 内存系统的重排序。由于处理器采用缓存和读、写缓冲区,这使得加载和存储操作看上去是在乱序执行。
2、3同属于处理器重排序
针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障来禁止某些特殊的处理器重排序
数据依赖性
定义:如果两个操作访问同一个变量,且其中一个为写操作,此时这两个操作就存在数据依赖性
例:
double pi = 3.14 //A
double r = 1.0 //B
double area = pi * r * r //C
这是一个计算圆面积的代码,其中AB没有任何关系,对最终结果也不会造成影响,因此他们之间的执行顺序可以重排序,所以执行顺序可以使A-B-C或B-A-C
编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序
as-if-serial
as-if-serial语义的意思是不管怎么重排序,(单线程)程序的执行结果不能被改变。
编译器和处理器都必须遵守as-if-serial语义。这产生了一种程序按顺序执行的错觉
happens-before
JMM提供了八条规则,去推论跨线程的内存可见性问题问题
- 程序顺序规则:一个线程的每个操作,happens-before于该线程的任意后续操作
- 监视器锁规则:对一个锁的解锁,happens-before于这个锁的加锁
- volatile规则:对一个volatile域的写,happens-before于后续任意对这个volatile域的读
- 传递性:如果A happens-before于B,B happens-before于C,那么A happens-before于C
- start()规则:如果线程A内启动线程B,那么ThreadB.start() happens-before于B内的任何操作
- join()规则:如果线程A执行线程B的join操作并成功返回,那么线程B内的所有操作都happens-before于返回操作
- 程序终端规则:对对象的interrupt操作happens-before于被中断代码检测到中断事件的发生
- 对象终结规则:一个对象初始化happen-before于finalize
as-if-serial和happens-before的区别
就是as-if-serial是保证单线程执行结果不改变的,而happens-before保证正确同步的多线程的执行结果不被改变
重排序案例
static int x = 0;
static int y = 0;
static int a = 0;
static int b = 0;
public static void main(String[] args) throws InterruptedException {
x = 0;y=0;a=0;b=0;
for (int i = 0; i < 100; i++) {
Thread one = new Thread() {
public void run() {
a = 1;//1
x = b;//2
}
};
Thread two = new Thread() {
public void run() {
b = 1;//3
y = a;//4
}
};
one.start();
two.start();
System.out.println(x + " " + y);
}
}
可能得到结果(1,0),(1,1),(0,1),事实上按照JMM的特性还可能得到(0,0)
其中1、2和3、4之间没有数据依赖符合as-if-serial原则,因此存在重排序,即2先于1执行,4先于3执行,而又由于多线程并发执行,最后执行顺序可能为4213(只要24都先于13)时,结果为(0,0)