volatile型变量语义讲解一 :对所有线程的可见性


一、volatile变量语义一的概念

  当一个变量被定义成volatile之后,具备两个特性:

  特性一:保证此变量对所有线程的可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量并不能做到这一点,普通变量的值在线程传递时均需要通过主内存来完成。 比如:线程A修改了一个普通变量的值,然后向主内存进行回写,另一条线程B在线程A回写完成了之后再对主内存进行读取操作,新变量值才会对线程B可见。

二、volatile能够保证线程安全吗

  基于volatile变量在各个线程中是不存在一致性问题的,从物理存储的角度看,各个线程的工作内存中volatile变量也可以存在不一致的情况,但是由于每次使用前都要进行刷新,执行引擎看不到不一致的情况,因此也可以人为不存在一致性问题,但是java里面的运算操作符并非是原子操作,这导致了volatile变量的运算在并发下一样是不安全的

案例代码:

/**
 * 测试Volatile的特性
 */
public class VolatileTest {
public static volatile  int race = 0;

public static void increase(){
    race++;
}
//定义线程的数量
private static final int THREADS_COUNT = 20;

public static void main(String[] args) {
    Thread[] threads = new Thread[THREADS_COUNT];
    for(int i = 0;i<THREADS_COUNT;i++){
        threads[i] = new Thread(new Runnable() {
            @Override
            public void run() {
               for(int j = 0;j<1000;j++){
                   increase();
               }
            }
        });
        threads[i].start();
        System.out.println("线程"+i+"开始执行");
    }

    while (Thread.activeCount()>2){
        System.out.println("Thread.activeCount() = "+Thread.activeCount());
        Thread.yield();//有其他线程等待时,将该线程设置为就绪状态。
    }
    System.out.println("race:"+race);
}
}

  这段代码发起了20个线程,每个线程都对race变量的做了10000次的自增操作,如果是正常的并发的话,那么race的结果用该是200000,可是执行几次,发现结果并不是200000,而都是一个小于200000的值。这是为什么呢? 因为++操作本身就不是原子的,要经过读取计算和写回,那么,我们通过一张图模仿一下以上代码:

 

 

 

 

  由于变量被volatile修饰,因此这张图中的3,4操作是连续不间断的,5,6,7的操作也是连续不间断的,但是经过两个线程的读取修改写回操作后,i的值仅仅从1变为了2,并不是我们想象的3,

可能在这里理解上述的图和描述有点抽象,因为有的朋友可能并不能理解数据在主存和缓存中的读取更改的传递规则,在这里,补充一下变量在内存之间的相互操作知识点,大家可以先看以下这块内容,再回过头进行理解上述图中的操作。

三:内存间的相互操作

·lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

·unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

·read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

·load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

·use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

·assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

·store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

·write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

 

 

   由volatile修饰的变量的特性保证此线程的可见性可知,当我们使用volatile修饰了一个变量后,一个线程对此变量的修改对于其他线程来讲是立即可知的,也就是说.assign,.store,.write这三个操作是原子的,中间不会间断,会马上的同步主存,就像直接操作主存一样,并通过缓存一致性通知其他的缓存中的副本过期。普通变量可能会在.assign,.store,.write这三个操作中插入其他的操作,导致更改后的数据不能立即同步回主存,这种情况在volatile修饰变量时是不存在的。

四:使用volatile控制并发的场景

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized、java.util.concurrent中的锁或原子类)来保证原子性:

1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

2、变量不需要与其他的状态变量共同参与不变约束。

举个例子:


class VolatileOne{
volatile boolean isShutDown;

public void shutDown(){
    isShutDown = true;
}


public void dowork(){
    while (!isShutDown){
        //业务代码
    }
}
}
这类场景就比较适合使用volatile控制并发,当 shutDown()方法被调用时,能保证所有线程中执行的dowork()方法都立即停下来。

使用volatile变量的第二个特性是禁止指令重排优化,我们下一篇再来分析。

 


 

补充一下,本文参考资料来源于:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》,其中讲解race++操作时书上用了字节码进行解释的,我这里通过搜索资料,通过一张图来进行的描述,希望可以直观的帮助大家理解。

还有,昨天写的音乐+技术+逗比的结合写法被朋友说写的太乱了,很无厘头,因此这篇中规中矩的写了一篇,整理加自身理解,作者整理不易,觉得能帮助到您的请动动小手点个赞,如有问题请评论区提出。

版权声明:本文为zaevn00001原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/zaevn00001/p/13809794.html