内存模型
在讲述并发编程之前,我么首先要先了解内存模型
计算机执行指令,每条指令都在cpu中执行
cpu执行速度很快,内存的读写相对慢 ,因此cpu中有高速缓存
当程序在运行过程中,会将运算需要的数据从主存复制一份到cpu的高速缓存当中,cpu直接从里面读取数据,计算结束后将数据刷新到主存中
举个简单的栗子:
i = i + 1;
当线程执行这段语句时会分为5步:
- 先从主存当中读取i的值
- 然后复制一份到高速缓存当中
- 然后cpu执行指令对i进行加1操作
- 然后将数据写入高速缓存
- 最后将高速缓存中i最新的值刷新到主存当中
上述语句的执行在多线程中存在缓存一致性的问题:
多核cpu中,每条线程可能运行于不同的cpu中,因此每个线程运行时有自己的高速缓存,由此,上述语句在多线程中变量i在多个cpu中都存在缓存,这时就出现了缓存一致性的问题
被多个线程访问的变量为共享变量
那么,如何解决缓存一致性的问题?
两种方式:
- 通过在总线加LOCK#锁的方式
- 通过缓存一致性协议
这两种方式其实都是通过硬件层面来处理的
由于在总线加LOCK#锁的方式会使得效率低下,所以出现了后来的缓存一致性协议
最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的
核心思想:
当cpu写数据时,如果发现操作的变量是共享变量,即在其他cpu中也存在该变量的副本,会发出信号通知其他cpu,将该变量的缓存行置为无效状态,因此当其他cpu需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取
并发编程
ok,了解完内存模型后我们来看并发编程
并发编程中我们通常会遇到三个问题:
- 原子性问题
- 可见性问题
- 有序性问题
原子性
一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
怎么理解?举一个简单的栗子:
int i;
i = i + 1;
前面我们已经知道这段代码的执行需要分五个步骤,假设在第一步的时候代码执行的操作突然间断,此时另外一个线程正好读取i的值,那么取到的是0而非期望的1,由此可知并发编程时需要满足原子性
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
举个简单的栗子:
// 线程1
int i;
i = i + 1;
// 线程2
int j = i;
线程1中执行完i = i + 1;
代码时,会将cpu1中的高速缓存当中i的值变为1,这时并没有立即刷新主存中i的值,此时线程2恰好执行int j = i;
取到的i值还是主存中的0并不是期望的1。线程2没能立即看到线程1中修改共享变量之后的值,这就是并发编程中的可见性问题
有序性
程序执行的顺序按照代码的先后顺序执行
举一个简单的栗子:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上述代码,语句1是在语句2之前,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么?这里出现了一个新的概念:指令重排序
指令重排序:处理器为了提高效率,可能会对代码进行优化,它不保证各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
简单的讲,为了效率会对代码执行顺序重排并且不影响结果
这里就会有另外一个问题:靠什么保证结果的一致性?
举一个简单的栗子:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
其中一个可能的执行顺序:
2-->1-->3-->4
那么
2-->1-->4-->3
这种顺序有可能吗?
不可能,处理器在指令重排时会考虑数据的依赖性,栗子中的语句4依赖了语句3,那么它就会保证这两句的顺序执行来保证结果的一致
上述的考虑是在单线程的情况下,那么多线程呢?
来,再吃一个栗子:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
语句1和语句2没有相互依赖,那么就有可能发生2先于1执行,那么紧接着线程2执行,此时会跳过while循环直接执行doSomethingwithconfig(context);
,那么此时context是null就会出现问题了
所以,并发编程中会存在有序性的问题
总结
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性,三者缺一不可