Volatile关键字详解(二)

一、volatile关键字的含义

一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,就具备了两层含义:

(1)保证了修饰的变量在线程之间具有”可见性”(即在一个线程对变量进行修改,能够在另外一个线程感知到变量被修改);

(2)禁止指令的重排序。

代码演示讲解:假如线程1先执行,线程2后执行。

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;

这段代码执行的时候,一定可以将线程中断吗?答案是不一定!也许很多时候,这段代码可以将线程中断,但是也存在无法将线程中断的情况(虽然发生的可能性很小,但是一旦发生这种情况会造成死循环)。

每个线程在运行的时候,都有自己的工作内存,线程1在运行的时候,会将stop变量的值拷贝一份到自己的工作内存中。那么当线程2在运行的时候,虽然改变了stop变量的值,但是可能没有及时将更改后的值放入主内存中,就去执行别的任务了。故线程1不知道线程2已经修改了stop的值,因此会一直循环下去。

如果使用volatile关键字来修饰stop变量,会产生不一样的情况?

1、使用volatile关键字会将修改的结果立即写入主内存中;

2、使用volatile关键字,当线程2进行修改的时候,会导致线程1工作内存中的缓存变量stop的缓存行无效(反映到硬件的话,就对应CPU的L1或L2的缓存中对应的缓存行无效);

3、由于线程1中的stop值无效,那么线程1会再次去主内存中读取变量stop的值。

那么在线程2修改stop值时(当然包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。所以线程1读取到的就是最新的正确的值。

二、volatile能保证原子性吗?

从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?答案是不能!

可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存,就是说自增操作的三个子操作可能会分割开执行。

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap)。(后续会介绍CAS操作)

三、volatile能保证有序性吗?

在前面提到volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

四、volatile实现原理

有volatile变量修饰的共享变量进行写操作的时候,会多出一行以Lock为前缀的汇编代码,

这个前缀指令会在多核处理器下引发两件事情:

1、将当前处理器缓存行的数据写回到系统内存

2、这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。

所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,就会重新从系统内存中把数据读到处理器缓存里。

五、volatile使用场景

通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

1、使用volatile用作状态标记量

在该场景中,应用程序的某个状态由一个线程设置,其他线程会读取该状态并以该状态作为其计算的依据( 或者仅仅读取并输出这个状态值。此时使用 volatile变量作为同步机制的好处是一个线程能够 “通知” 另外一个线程某种事件( 例如,网络连接断连之后重新连上)的发生,而这些线程又无须因此而使用锁,从而避免了锁的开销以及相关问题。

2、 使用 volatile保障可见性

在该场景中,多个线程共享一个可变状态变量 ,其中一个线程更新了该变量之后。其他线程在元须加锁的情况下也能够看到该更新。

3、使用 volatile变量替代锁

volatile 关键字并非锁的替代品,但是在一定的条件下它比锁更合适 ( 性能开销小 、代码简单 )。多个线程共享一组可变状态变量的时候,通常我们需要使用锁来保障对这些变量的更新操作的原子性,以避免产生数据不一致问题。利用 volatile 变量写操作具有的原子性 ,我们可以把这一组可变状态变量封装成一个对象,那么对这些状态变量的更新操作就可以通过创建一个新的对象并将该对象引用赋值给相应的引用型变量来实现。在这个过程中, volatile 保障了原子性和可见性。从而避免了锁的使用。

猜你喜欢

转载自blog.csdn.net/qq_40303781/article/details/85759584