前言
在多线程中,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。下面依次介绍下这三个特性。
概念与理解
原子性:在一个操作中cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。例如:a =0;这个操作就是原子的,无法再拆分了。但a = a +1;就不是原子性的操作了,在实际程序运行过程中,会拆分为:1、从主内存读取a的初始值2、进行a+1操作3、对a重新赋值
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
举个栗子:
//主线程
int a =0;
//线程1
a =2;
//线程2
b =a +1;
有变量a,初始值为0,假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 a=2这句时,会先把a的初始值加载到CPU1的高速缓存中,然后赋值为2,那么在CPU1的高速缓存当中i的值变为0了,却没有立即写入到主存当中。
此时线程2执行 b = a,它会先去主存读取a的值并加载到CPU2的缓存当中,注意此时内存当中a的值还是0,那么就会使得b的值为1,而不是3.
这就是可见性问题,线程1对变量a修改了之后,线程2没有立即看到线程1修改的值。
解决:
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。
有序性:即程序执行的顺序按照代码的先后顺序执行。
看下面代码:
int a =0;
long b =0;
a = 1; //语句1
b = 2; //语句2
从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:
//线程1
int a =0;
bollean b=false;
a = 1;
b = true;
//线性2
while(!b){
sleep()
}
doSomethingWithA(a);
这个例子只是为了表达一下可见性(实际是有问题的),在线程1中a=1,然后b=true,这两个操作在JVM中完全是可以颠倒顺序的,而线程2中b=ture,退出循环,使用a来做操作,此时a=0,就会发生问题。
网友评论