对volatile的一点lijie

Posted timetellu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对volatile的一点lijie相关的知识,希望对你有一定的参考价值。

  理解volatile其实还是有点儿难度的,它与Java的内存模型有关,所以在理解volatile之前需要先了解有关Java内存模型的概念,目前只做初步的介绍。

一、操作系统语义

  计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。

  我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。

  CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。

1、产生数据一致性 / 内存不可见性问题

  有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:内存不可见性 ——> 数据一致性。

  线程在运行的过程中会把主内存的数据拷贝一份到线程内部cache中,也就是working memory。这个时候多个线程访问同一个变量,其实就是访问自己的内部cache,不再与主存打交道,只有当运行结束后才会将数据刷新到主存中。

技术图片

  举一个简单的例子】:

public class VariableTest 

    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException 
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();

        new Thread(threadA, "threadA").start();
        Thread.sleep(1000l);//为了保证threadA比threadB先启动,sleep一下
        new Thread(threadB, "threadB").start();


    

    static class ThreadA extends Thread 
        public void run() 
            while (true) 
                if (flag) 
                    System.out.println(Thread.currentThread().getName() + " : flag is " + flag);
                    break;
                
            

        

    

    static class ThreadB extends Thread 
        public void run() 
            flag = true;
            System.out.println(Thread.currentThread().getName() + " : flag is " + flag);
        
    

  运行结果:(光标一直在闪)

技术图片

  上面例子出现问题的原因在于:

      (1)线程A把变量flag(false)加载到自己的内部缓存cache中;线程B修改变量flag后,即使重新写入主内存,但是线程A不会重新从主内存加载变量flag,看到的还是自己cache中的变量flag。所以线程A是读取不到线程B更新后的值,然后一直死循环...

      (2)除了cache的原因,重排序后的指令在多线程执行时也有可能导致内存不可见,由于指令顺序的调整,线程A读取某个变量的时候线程B可能还没有进行写入操作呢,虽然代码顺序上写操作是在前面的。

2、解决缓存一致性方案

  1.  通过在总线加LOCK#锁的方式
  2.  通过缓存一致性协议

  但是方案1存在一个问题:它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行,其他CPU都得阻塞,效率较为低下。

  第二种方案:缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。

        其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载

                数据。  

                           技术图片

二、Java内存模型

1、共享变量

     共享变量是指:可以同时被多个线程访问的变量,共享变量是被存放在堆里面,所有的方法内的临时变量都不是共享变量。

2、重排序

  重排序是指为了提高指令运行的性能,在编译时或者运行时对指令执行顺序进行调整的机制。重排序分为编译重排序和运行时重排序。

  编译重排序是指:编译器在编译源代码的时候就对代码执行顺序进行分析,在遵循as-if-serial的原则前提下对源码的执行顺序进行调整。

  运行时重排序:是指为了提高执行的运行速度,系统对机器的执行指令的执行顺序进行调整。

 【as-if-serial原则】是指在单线程环境下,无论怎么重排序,代码的执行结果都是确定的。

3、可见性

  内存的可见性是指线程之间的可见性,一个线程的修改状态对另外一个线程是可见的,用通俗的话说,就是假如一个线程A修改一个共享变量flag之后,则线程B去读取,一定能读取到最新修改的flag。

    (看上边示例)  

  Java提供了volatile来保证可见性。

  当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。 

  当然,synchronize和锁都可以保证可见性。

4、原子性

  原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。就像数据库里面的事务一样,他们是一个团队,同生共死。

  在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java只保证了基本数据类型的变量和赋值操作才是原子性的。

  想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。

// 一个很经典的例子就是银行账户转账问题:
      比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。试想一下,如果这2个操作不具备原子性,会造成什么样的后果。            
      假如从账户A减去1000元之后,操作突然中止(B此时并没有收到)。
      然后A又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元(少了500)。

      所以这2个操作必须要具备原子性(不可分割的最小操作单位)才能保证不出现一些意外的问题。  

三、volatile原理

  (1)volatile修饰的变量不允许线程内部cache缓存和重排序,可以保证内存的可见性和数据的一致性。

   (2)线程读取数据的时候直接读写内存,同时volatile不会对变量加锁,因此性能会比synchronized好。

  另外还有一个说法是使用volatile的变量依然会被读到线程内部cache中,只不过当B线程修改了flag后,会将flag写回主内存,同时会通过信号机制通知到A线程去同步内存中flag的值。

 volatile相对于synchronized稍微轻量些,在某些场合它可以替代synchronized,但是又不能完全取代synchronized,只有在某些场合才能够使用volatile。
使用它必须满足如下两个条件:
// 1、对变量的写操作不依赖当前值; // 2、该变量没有包含在具有其他变量的不变式中
 volatile经常用于两个两个场景:状态标记、double check(单例模式)

 

以上是关于对volatile的一点lijie的主要内容,如果未能解决你的问题,请参考以下文章

volatile关键字

volatile底层实现原理

对Java单例模式 volatile关键字作用的理解

Java并发编程-volatile可见性的介绍

Java并发编程--Volatile详解

转载Java并发编程:volatile关键字解析(写的非常好的一篇文章)