并发编程中的原子性问题,可见性问题,有序性问题。

Posted guo10432

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程中的原子性问题,可见性问题,有序性问题。相关的知识,希望对你有一定的参考价值。

原子性问题:
在一个线程中,对一个32的二进制数进行赋值操作,当低16位的数据写入后,发生了中断,而此时又有一个线程去读取这个写入的数据,必定得到的是一个错误的数据。
在java中这种情况是不存在的,因为对基本数据类型的写入和赋值保证了原子性(i=10)。但仅限制于对基本数据类型,而变量的赋值就不能保证,自增就是个很好的例子,
i++:
自增:先读取数据,再加1,再写入.
也就是说只有简单的读取,赋值才是原子性。而且赋值必须是将数字赋给某个变量,变量之间的相互赋值不是原子性的,如果要实现更大范围操作的原子性,可以通过可以通过synchronized和Lock来实现。
由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
 
可见性问题:
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
当程序运行过程中,会将需要的数据从主内存复制一份给cpu的高速缓存,cpu进行计算时就可以直接从高速缓存读取和写入数据,运算结束,把结果刷新到主存中。
线程一:int i=0; i=10;   线程二:j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。                      
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
在java中,volatile关键字可以保证可见性。
  当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
 
有序性问题:
int a = 10;    //语句1
int r = 2;    //语句2
a = a + 3;    //语句3
r = a*a;     //语句4
这四条语句不一定按照顺序执行。
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,进行指令重排序。它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?
指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。
1.volatile关键字的两层语义
  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2)禁止进行指令重排序。
  但是用volatile修饰之后就变得不一样了:
  第一:使用volatile关键字会强制将修改的值立即写入主存;
  第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
  第三:由于线程1的工作内存中缓存变量缓存行无效,所以线程1再次读取变量的值时会去主存读取。
  那么在线程2修改值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
volatile没办法保证对变量的操作的原子性。
  在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
  volatile关键字禁止指令重排序有两层意思:
  1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
//x、y为非volatile变量
//flag为volatile变量
x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;         //语句4
y = -1;       //语句5
由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。
并且volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
 
 
 
 
 
 
 
 

以上是关于并发编程中的原子性问题,可见性问题,有序性问题。的主要内容,如果未能解决你的问题,请参考以下文章

并发编程三要素:原子性,有序性,可见性

voliate怎么保证可见性

Java高并发编程实战2,原子性可见性有序性,傻傻分不清

7.三大性质总结:原子性可见性以及有序性

整理一下《java并发编程实战》中的知识点

Java并发基础并发编程bug源头:可见性原子性和有序性