并发基础

Posted by ShiYu on 2017-09-24

Java内存模型中,内存分为两类:main memorey和working memory,main memory是线程共享的,working memory中存放的是线程所需变量的拷贝,线程若要对main memory中内容进行操作,需要先拷贝到自己的working memory。

基于上述模型,引发出多线程共享变量时会发生的同步问题,引发同步问题的因素有3点:

可见性

假设一个对象中有一个变量i,i是保存在main memory中的,当一个线程要操作i时,首先需要从main memory中将i拷贝到自己的working memory中,然后此线程便可以在自己的working memory中修改i,修改结束后再将i从working memory 写回main memory中,此时新的i值才能被其它线程读取。当一个线程修改了变量值,修改后的值能立刻被其它线程读取即是可见性。

所以当线程无法保证它从读取到的i值时最新的时,即无法满足可见性时就会出现同步问题,例如i被线程A读取,经过一系列逻辑后i的值加了1,但是在线程A尚未将新值写入main memory中时,线程B从main memory将i拷贝到了自己的working memory中,此时线程B读取到的值便是所谓的脏数据,因为实际上i已经被线程A加了1。

Java中提供了volatile关键字保证可见性,此外,通过synchronized或lock也可保证可见性,这两个关键字能保证同一时刻只有一个线程获取到锁然后执行同步代码,并且在释放锁之前,对变量的修改刷新到main memory中。因此可以保证可见性。

原子性

原子性是指一个操作或多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就不执行。
对应到具体的Java代码来解释一下:

1
2
3
int x=10;//语句1
int y=x;//语句2
x++; //语句3

Java中,对于基本数据类型的变量读取和操作是原子性操作,即这些操作是不可被打断的,要么执行,要么不执行。上述代码乍一看,似乎两句都是原子性的,其实只有语句1是原子性的,语句2实际包含了两个操作,它要先读取x的值,然后将x值写入内存,虽然两步操作每一步都是原子性 的,但合并到一起就不是了。语句3包含了3步操作,读取x值,将x值+1,写入新的值。

Java内存模型只保证对基本类型的基本读取和赋值是原子操作,如果需要更大范围的原子性操作,可以通过synchronized或lock来实现,因为这两个关键字能够保证任一时刻只有一个线程执行该代码块,自然就不存在原子性问题。

有序性

Java内存模型中,允许编译器和处理器对指令进行重新排序,但是重新排序过程不会影响到单线程的执行,却会影响到多线程并发执行的正确性。

1
2
3
int a=10;//语句1
int r=2;//语句2
a+=3;//语句3

上述代码在编译过程中,可能会出现重排序,排序后执行的顺序是:语句2,语句1,语句3。虽然执行的顺序变了,但是最终结果依然是正确的,这在单线程环境中是没问题的,但是如果在多线程环境中就可能出现问题。

1
2
3
4
5
6
7
8
9
//线程1
context=loadContext();//语句1
inited=true;//语句2

//线程2
while(!inited){
sleep();
}
doSomething(conext);

由于线程1中语句1和语句2没有依赖性,可能会被重新排序,加入发生重排序,语句2在语句1之前执行,线程2会错误地判断初始化已经完成,从而跳出while循环,而此时context并没有被初始化,程序就会报错。

Java中可通过volatile关键字保证一定的“有序性”,另外可以通过synchronized或lock来保证有序性。

想要并发程序正确执行,必须保证原子性、可见性、有序性。只要有一个没有保证,就可能会导致程序运行的不正确。