volatile是java中关键词之一,作为一种轻量级同步机制,在多线程中经常会被使用。被volatile修饰的变量,具有可见性、有序性,不具备原子性。
原子性:
指不可中断的一个或一系列操作,即这些操作是不可被中断的,要么全部执行完,要么不执行,若只执行一部分,那么就不具备原子性。在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,比如:
int x = 1;
int y = x;
x++; x = x + 1
上面中只有第1行是原子操作,直接将数字1赋值给x,也就是直接写入对应内存中。而2~4行中,都有一个先读取x值,再赋值写入2个步骤,所以不具备原子性。这样就会有一个问题,当一个线程完成读取还未赋值之时,另一个线程完成了一个新的赋值,那么第一个线程再完成赋值时,得到的数据就会有问题了。volatile不保证原子性。比如:
volatile int i=0
//线程A
i++;
System.out.print(i);
//线程B
i++;
System.out.print(i);
打印的结果,线程A和线程B打印的结果,不会是递增了,有可能会有一样的值。这是以为所以使用了volatile后,i值的修改回立即更新到主内存中,对其他线程立即可见,但是其他线程再计算的时候,还是工作内存的老值,所以会造成重复更新到主内存中。
PS:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。
可见性:
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。每个线程都有自己的工作内存(类似于高速缓存),线程对变量的所有操作都必须在工作内存(高速缓存)中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。比如:
i=0
//线程A
i=1
//线程B
j=i
假设线程A是在cpu1中执行,cpu2执行线程B。那么当执行到i=1时,会先白此值加载到工作内存(cach)中去,但却没有立即写入主内存中。此时线程B执行了j=i,那么他会先去读主内存的i值并加载到cpu2中的cach中,因为在读取的时候,i还为0,不是1,;所以此时j的打印值也是0,不会是改变后的1。而被volatile修饰的共享变量具有可见性,它会保证修改工作内存的值会被立即更新到主内存,当有其他线程需要读取时,它会去主内存中读取新值到自身的工作内存。而普通变量做不到立即更新到主内存(可百度Java内存模型)。
有序性:
有序性是指程序按照代码的先后顺序执行。比如:
int a = 1;
int b = 2;
a = a + 3;
b = a + 4;
从上面代码的顺序看,第1行一定是在第2行之前执行的吗?不一定,因为指令重排的原因,2可能是在1之前执行的。
那么什么是指令重排呢?一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。(可百度查看as-if-serial)
上面的例子中,第1行和第2行的执行顺序对程序最终结果是没有影响的。所以在执行过程中,第2行有可能会先于1执行。那么第4行会先于3执行么,我们都知道运行结果是不会的,那为什么呢?那是因为虽然处理器会对指令进行重排序,但是重排指令时会考虑指令之间的数据依赖顺序的。上面的例子中,第三行一定是在第四行之前执行的。虽然重排序不会影响单个线程内程序执行的结果,但是同时在多线程之间的数据依懒性不被考虑,所以还是会指令重排,影响结果。比如:
class TestVolatile{
int a = 10;
/*volatile*/ boolean flag = false;
public void changeStatus(){
a = 20;
flag = true;
}
public void run(){
if(flag){
int b = a + 3;
System.out.print(b);
}
}
}
/***main.java***/
TestVolatile mVolatile = new TestVolatile();
//线程A
mVolatile.run();
//线程B
mVolatile.changeStatus();
上述结果中,打印的b有可能是13,而不是23。这是因为线程B中a的赋值和flag的赋值没有依赖性,所以在执行changeStatus方法时,发生了指令重排,先执行了flag=true。线程A此时执行run方法,flag为true,但是a还没有赋值为20,此时还是为10,所以打印的b为13,而非23。
如果共享变量定义为volatile,那么会禁止指令重排。赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(也成内存栅栏),指令重排序时不能把后面的指令重排序到内存屏障之前的位置(如上面的例子是不会把flag=true后,才会执行if(flag)方法),只有一个CPU访问内存时,并不需要内存屏障;
内存屏障会具有3大功能:
1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2、它会强制将对缓存的修改操作立即写入主存;
3、如果是写操作,它会导致其他CPU中对应的缓存行无效。
参考:
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.cnblogs.com/zhengbin/p/5654805.html
https://blog.csdn.net/u012723673/article/details/80682208