一文搞懂volatile的可见性原理

  说volatile之前,了解JMM(Java内存模型)有助于我们理解和描述volatile关键字。JMM是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让Java程序在各种平台下都达到一致的内存访问效果。JMM也可以称之为Java线程内存模型,也描述了Java线程在工作中对数据的操作过程以及描述了线程之间的通信过程。

  以上便是JMM的基本逻辑图,Java采用工作内存和主内存进行数据交互的原因可以解释为,工作内存一般为cpu的高速缓存,cpu的高速缓存就是为了解决cpu日益增长的速度与主存不匹配导致浪费计算资源,所以线程的工作内存位于cpu的高速缓存中来提高运算速度。但是多个线程在对主内存中共享变量操作时会有一个可见性问题。具体可看以下代码:

package xyz.ring2.demo.test;

public class VolatileVisibilityTest {
    public static boolean flag = false;

    public static void changeCondition(){
        flag = true;
    }

    public static  void main(String[] args) throws InterruptedException {
        System.out.println("working and waiting for change...");
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!flag){
                    System.out.println("hello");
                }
            }
        }).start();

        Thread.sleep(200);

       new Thread(new Runnable() {
            @Override
            public void run() {
                changeCondition();
                System.out.println("condition has changed.");
            }
        }).start();

        Thread.sleep(200);
        System.out.println("work done.");

    }

}

  在该程序中有一个共享变量flag,第一个线程运行时等待别的线程改变flag的值使其跳出循环,第二个线程是去改变共享变量flag的值。在我们看来,第一个线程只需要等待第二个线程改变了flag的即可跳出循环。以下是程序运行结果:

   可以看到当“work done”打印出来时程序还没有停止,此时我们可以得出结论。两个线程对共享变量的操作是互相不可见的。此时我们很自然的想到了通过加synchronizedJava内置锁来解决。

通过在while循环外添加synchronized(this)同步块确实能解决这种问题,但是在这种仅仅只需要保证一个共享变量可见的情况下采用synchronized锁来保证同步代价太大,此时我们应该采用Java所

提供的volatile关键字来保证变量的可见性。使用上通过在flag前加上volatile关键字即可。

public static volatile boolean flag = false;

以下是运行结果:

正常的使程序结束了,线程一成功的感知到了线程二对flag变量的改变。

  那么volatile关键字使如何保证多线程下共享变量线程间可见的呢?

首先我们来了解以下JMM中的数据原子操作:

  • read(读取):从主内存读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • use(使用):从工作内存读取数据来计算
  • assign(赋值):将计算好的值从新赋值到工作内存中
  • store(存储):将工作内存数据写入到主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定):将主内存变量加锁,标识为线程独占状态
  • unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

  JVM通过以上原子操作来处理主内存和工作内存中的数据交互。那么volatile到底是如何保证的呢?

  Java中的volatile关键字是通过调用C语言实现的,而在更底层的实现上,即汇编语言的层面上,用volatile关键字修饰后的变量在操作时,最终解析的汇编指令会在指令前加上lock前缀指令

来保证工作内存中读取到的数据是主内存中最新的数据。具体的实现原理是在硬件层面上通过:MESI缓存一致性协议:多个cpu从主内存读取数据到高速缓存中,如果其中一个cpu修改了数据

,会通过总线立即回写到主内存中,其他cpu会通过总线嗅探机制感知到缓存中数据的变化并将工作内存中的数据失效,再去读取主内存中的数据。

 IA32架构软件开发者手册对lock前缀指令的解释:
  1.会将当前处理器缓存行的数据立即回写到系统内存中,
  2.这个写回内存的操作会引起其他cpu里缓存了该内存地址的数据失效(MESI协议)

 

  现在我们知道了volatile可以保证变量的可见性,我们还应该知道volatile不可以保证原子性:

  volatile无法保证原子性:如:两个线程同时read主内存中相同的值,load到工作内存中,两个线程的cpu又同时use了count值并进行了计算且assign回工作内存,但其中一个线程通过总线store回主内存的
  速度更快,于是由于(总线)MESI缓存一致性协议下的cpu总线嗅探机制就会使得另一个线程工作内存中的变量副本失效,导致之前的操作结果丢失(可以结合图片理解)。

  并发编程的三大特性:可见性,原子性,有序性。那么volatile对有序性又是怎样的呢。。。这涉及到happens-before规则,volatile关键字可以体统屏障保护,使得编译器和jvm对变量操作的重排序失效。

可以读取我的另一篇文章:单例模式值双检索 来理解一下重排序所带来的问题。

 

posted @ 2019-08-29 14:40  ring977  阅读(2028)  评论(0编辑  收藏  举报