有关并发编程

2018-02-09  本文已影响9人  34sir

内存模型

在讲述并发编程之前,我么首先要先了解内存模型

计算机执行指令,每条指令都在cpu中执行
cpu执行速度很快,内存的读写相对慢 ,因此cpu中有高速缓存
当程序在运行过程中,会将运算需要的数据从主存复制一份到cpu的高速缓存当中,cpu直接从里面读取数据,计算结束后将数据刷新到主存中

举个简单的栗子:

i = i + 1;

当线程执行这段语句时会分为5步:

上述语句的执行在多线程中存在缓存一致性的问题:
多核cpu中,每条线程可能运行于不同的cpu中,因此每个线程运行时有自己的高速缓存,由此,上述语句在多线程中变量i在多个cpu中都存在缓存,这时就出现了缓存一致性的问题
被多个线程访问的变量为共享变量

那么,如何解决缓存一致性的问题?
两种方式:

这两种方式其实都是通过硬件层面来处理的

由于在总线加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就会出现问题了

所以,并发编程中会存在有序性的问题

总结

要想并发程序正确地执行,必须要保证原子性、可见性以及有序性,三者缺一不可

上一篇下一篇

猜你喜欢

热点阅读