volatile

    volatile作为java中的关键词之一,用以声明变量的值可能随时会被别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)volatile会禁止指令重排。

    volatile具有可见性、有序性,不具备原子性。

    volatile修饰的变量不允许线程内部缓存和重排序即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

     下面来分别看下可见性、有序性、原子性:

     原子性:如果你了解事务,那这个概念应该好理解。原子性通常指多个操作不存在只执行一部分的情况,如果全部执行完成那没毛病,如果只执行了一部分,那对不起,你得撤销(即事务中的回滚)已经执行的部分。

     可见性:当多个线程访问同一个变量x时,线程1修改了变量x的值,线程1、线程2...线程n能够立即读取到线程1修改后的值。

     有序性:即程序执行时按照代码书写的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。(本文不对指令重排作介绍,但不代表它不重要,它是理解JAVA并发原理时非常重要的一个概念)。

    volatile适用场景

    适用于对变量的写操作不依赖于当前值,对变量的读取操作不依赖于非volatile变量。适用于读多写少的场景。可用作状态标志。JDK中volatie应用:JDK中ConcurrentHashMap的Entry的value和next被声明为volatile。

    volatile不会让线程阻塞,响应速度比synchronized高,这是它的优点。

   Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值

    在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

    当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

而声明变量是volatile的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

    1.保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。

    2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。

volatile 性能:

    volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,读多写少,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

     happens-before原则:

      看Java内存模型(JMM, Java Memory Model)时,总有一个困惑。关于线程、主存(main memory)、工作内存(working memory),我都能找到实际映射的硬件:线程可能对应着一个内核线程,主存对应着内存,而工作内存则涵盖了写缓冲区、缓存(cache)、寄存器等一系列为了提高数据存取效率的暂存区域。

      顺序一致性(Sequentially Consistent)内存模型。在这个模型里,所有操作按程序的顺序来执行,并且每一个操作都是原子的,且立即对所有线程可见。

       

       这个系统中同一时间只有一个线程能读或写内存。也就是说,这个系统里的每两个指令之间,都严格按执行的先后,具有着happens-before关系。所有的线程,都能够看到一致的全局指令执行视图。如果将总线1看做是线程和内存之间的通道,那么顺序一致性模型就相当于在所有读/写内存的操作时,锁住总线。
     对于多线程仍然存在问题。

     显然,顺序一致性模型是一种牺牲并行度、换取多线程对共享内存的可见性的一种理想模型。从JMM实现volatile以及synchronized的内存语义的方式,正是锁住总线或者说锁住线程自身存储(指working memory)。

       

java内存模型。

       可以看出,工作内存是一个明显区别于顺序一致性内存模型的地方。事实上,造成可见性问题的根源之一,就在于这个工作内存(强调一下,包括缓存、写缓冲和寄存器等等)。工作内存使得每个线程都有了自己的私有存储,大部分时间对数据的存取工作都在这个区域完成。但是我们写一个数据,是直到数据写到主存中才算真正完成。实际上每个线程维护了一个副本,所有线程都在自己的工作内存中不断地读/写一个共享内存中的数据的副本。单线程情况下,这个副本不会造成任何问题;但一旦到多线程,有一个线程将变量写到主存,其他线程却不知道,其他线程的副本就都过期。比如,由于工作内存的存在,程序员写的一段代码,写一个普通的共享变量,其可能先被写到缓冲区,那指令完成的时间就被推迟了,实际表现也就是我们常说的“指令重排序”(这实际上是内存模型层面的重排序,重排序还可能是编译器、机器指令层级上的乱序)。

     因此,在Java内存模型中,每个线程不再像顺序一致性模型中那样有确定的指令执行视图,一个指令可能被重排了。从一个线程的角度看,其他线程(甚至是这个线程本身)执行的指令顺序有多种可能性,也就是说,一个线程的执行结果对其他线程的可见性无法保证。

      插入指令重排序的问题:

               

            

            

            彩蛋:https://www.cnblogs.com/gotodsp/p/8836683.html

            从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM(Java Memory Model)通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

            重排序:

             现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。重排序的目的是为了性能。

              Example:
                           理想情况下:
                           过程A:cpu0—写入1—> bank0;
                           过程B:cpu0—写入2—> bank1;
                           如果bank0状态为busy, 则A过程需要等待
                           如果进行重排序,则直接可以先执行B过程。

                      

                  

              看一个程序:

                        

class ReorderExample {
    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;             //2
    }

    Public void reader() {
        if (flag) {              //3
            int i =  a * a;      //4
            ……
        }
    }
}

 

    flag为标志位,表示a有没有被写入,当A线程执行 writer 方法,B线程执行 reader 方法,线程B在执行4操作的时候,能否看到线程A对a的写入操作?

答案是: 不一定!

  由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序。

            如果操作1和操作2做了重排序,程序执行时,线程A首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

         往下看这个链接:https://www.jianshu.com/p/b4d4506d3585

      总结一下导致可见性问题的原因:

  1. 数据的写无法及时通知到别的线程,如写缓冲区的引入
  2. 线程不能及时读到其他线程对共享变量的修改,如缓存的使用
  3. 各种层级上对指令的重排序,导致指令执行的顺序无法确定

    所以要解决可见性问题,本质是要让线程对共享变量的修改,及时同步到其他线程。我们所使用的硬件架构下,不具备顺序一致性内存模型的全局一致的指令执行顺序,讨论指令执行的时间先后并不存在意义或者说根本没办法确定时间上的先后。可以看看下面程序,每个线程中的flag副本会在多久后被更新呢?答案是:无法确定,看线程何时刷新自己的工作内存。

    那么既然我们无法讨论指令执行的先后,也不需要讨论,我们实际只想知道某线程的操作对另一个线程是否可见,于是就规定了happens-before这个可见性原则,程序员可以基于这个原则进行可见性的判断。

    

public class testVisibility {
    public static boolean flag = false;

    public static void main(String[] args) {
        List<Thread> thdList = new ArrayList<Thread>();
        for(int i = 0; i < 10; i++) {
            Thread t = new Thread(new Runnable(){
                public void run() {
                    while (true) {
                        if (flag) {
                            // 多运行几次,可能并不会打印出来也可能会打印出来
                            // 如果不打印,则表示Thread看到的仍然是工作内存中的flag
                            // 可以尝试将flag变成volatile再运行几次看看
                                  System.out.println(Thread.currentThread().getId() + " is true now"); 
                        }
                    }
                }
            });
            t.start();
            thdList.add(t);
        }

        flag = true;
        System.out.println("set flag true");

        // 等待线程执行完毕
        try {
            for (Thread t : thdList) {
                t.join();
            }
        } catch (Exception e) {

        }
    }
}

那么既然我们无法讨论指令执行的先后,也不需要讨论,我们实际只想知道某线程的操作对另一个线程是否可见,于是就规定了happens-before这个可见性原则,程序员可以基于这个原则进行可见性的判断。

    volatile就是一个践行happens-before的关键字。看以下对volatile的描述,就不难知道,happens-before指的是线程接收其他线程修改共享变量的消息与该线程读取共享变量的先后关系。大家可以再细想一下,如果没有happens-before原则,岂不是相当于一个线程读取自己的共享变量副本时,其他线程修改这个变量的消息还没有同步过来?这就是可见性问题。
    

   其实仔细看看volatile的实现方式,实际上就是限制了重排序的范围——加入内存屏障(Memory Barrier or Memory Fence)。也即是说,允许指令执行的时间先后顺序在一定范围内发生变化,而这个范围就是根据happens-before原则来规定。内存屏障概括起来有两个功能:

  1. 使写缓冲区的内容刷新到内存,保证对其他线程/CPU可见
  2. 禁止读写操作的越过内存屏障进行重排序

 

     

参考文章:https://blog.csdn.net/zdxiq000/article/details/60874848

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值