volatile学习记录

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值