深入理解volatile

作者: 九点半的马拉 | 来源:发表于2019-07-02 15:04 被阅读0次

    Java内存模型

    在计算机中,所有的运算操作都是由CpU的寄存器来完成的,在CPU Cache模型没出来之前,CPU所访问的数据只能是计算机的主存,但CPU本身的计算速度与主内存的读写速度远远不一致,所以在中间添加了Cache模型,在程序运行的时候,程序会把从内存中读取的数据复制一份到Cache中,然后直接对CPU cache中的数据进行读取和写入,当运算结束后,再将CPU cache中的最新数据刷新到主内存中。

    cpu通过Cache与主内存进行交互.png

    cpu通过Cache与主内存进行交互.png

    但在多线程情况下,利用该机制,可能会出现缓存不一致的现象。
    典型的解决办法有:

    • 通过总线加锁,只允许一个CPU抢到总线锁,来访问这个变量的内存。

    • 通过缓存一致性协议
      例如:在读取操作时,不做任何处理,只是将Cache中的数据读取到寄存器中
      在写入操作时,通知其他CPU将该变量的Cache line置为无效状态,其他CPU在进行该变量读取时不得不到主内存中再次获取。

    • 共享变量存储在主内存当中,每个线程都可以访问。

    • 每个线程都有私有得到工作内存。

    • 工作内存只存储该线程对共享变量的副本。

    • 线程不能直接操作主内存,只有先操作了工作内存才能写入到主内存。

    java内存模型.png

    java内存模型.png

    并发编程的三个重要特性

    • 原子性
      指在一次的操作或多次的操作中,要么所有的操作都执行并不会受到任何元素的干扰而中断,要么所有的操作都不执行
      note: 两个原子性的操作结合在一起未必还是原子性的, 例如i++
    • 可见性
      当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值
    • 有序性
      是指程序代码在执行过程中的先后顺序,
      有时,处理器为了提高程序的运行效率,可能会对输入的指令做一定的优化,它不会完全按照代码的执行顺序进行,但是它会保证程序的最终运算结果是编码时所期望的那样,即指令的重排序
      在单线程情况下,无论怎样的重排序最终都会保证程序的执行效果和代码顺序执行的结果是完全一致的,但是在多线程情况下,如果有序性得不到保证,那么很有可能会出现很大的问题

    JMM与原子性

    在Java中,对基本数据类型的变量读取赋值操作都是原子性的,对引用类型的变量读取和赋值的操作也是原子性的。

    • x = 10 是原子性的
      首先将x=10写入到工作内存中,然后再将其写入到主内存中
    • y = x 是非原子性的
      1. 从主内存中读取x的值(如果x已经在执行线程的工作内存中,则直接获取)然后将其存入当前线程的工作内存中
      2. 在执行线程的工作内存中修改y的值为x,然后将y的值写入到主内存中
    • y++ 是非原子性的
      1. 执行线程从主内存中读取y的值(如果y已经存在了执行线程的工作内存中,则直接获取),然后将其内存当前线程的工作内存之中。
      2. 在执行线程工作内存中为y执行加1操作
      3. 将y的值写入到主内存中
    • z = z + 1 是非原子性的
      同上

    jMM与可见性

    java中提供了以下三种方式来保证可见性

    • 使用关键字volatile,对数据的读操作直接在主内存中读取(当然也会缓存到工作内存中,当其他线程对该共享线程进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先要修改工作内存,但是修改结束后会立即将其刷新到主内存中
    • 通过synchronized关键字能够保证可见性,同一时刻只有一个线程获得锁
    • 通过JUC提供的显式锁Lock也能保证可见性

    volatile关键字解析

    该类变量具备下面两层语义:

    1. 保证了不同线程之间对共享变量操作时的可见性,也就是说当一个线程修改volatile修饰的变量,另一个线程会立即看到最新的值。
    2. 禁止对指令进行重排序操作。
      但volatile不具备原子性
      有一个经典的例子 i++操作
      如果有两个线程执行该操作时,当一个线程读取到i的当前值后,停止,然后跳转到另一个线程读取i的值,然后执行+1操作,并将值返回到主内存中,有的读者可能会产生这样的疑惑:
      volatile具有可见性,当一个线程修改后,另一个线程会立即看到最新的值,所以当第一个线程暂停回来后会从主内存中读取到最新的值,并执行+1操作,但实际情况是两个线程的结果是一样的,比如i=10,两个线程执行 volatile i++操作,都得到了11,理想的结果是一个11,一个12,为什么呢?
      volatile的可见性保证你每次访问到该变量时,都会读取到最新的值,但是并不会更新你已经读的值,它也无法更新你已经读了的值。上文中第一个线程是读取后再停止的,此时i值还没有被修改,当另一个线程修改完成后,该线程继续执行接下来的i+1操作,此时的i已经是已被读的值了,不会到主内存中获取最新的值,保留的是最初的值,所以产生了错误。

    volatile的使用场景

    1. 开关控制利用可见性的特点
     public class MyThread extends Thread {
     private volatile boolean started = true;
     @Override
     public void run() {
         while(started) {
             
         }
     }
     public void shutdown(){
         this.started = false;
     }
    }
    
    1. 状态标记利用了顺序性特点
    private volatile boolean init = false;
    private Context context;
    public Context load() {
        if (!init) {
            context = loadContext();
            init = true; //防止重排序
        }
        return context;
    }
    
    1. Singleton设计模式的double-check也是利用了顺序性特点

    volatile和synchronized

    1. 使用上的区别
    • volatile关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量、常量。
    • synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块
    • volatile修饰的变量可以为null,synchronized关键字同步语句块的monitor对象不能为null
    1. 对原子性的保证
    • volatile无法保证原子性,但后者可以保证
    1. 对可见性的保证
      都能保证多线程间的可见性,但是实现机制不同。
    • synchronized借助JVM指令monitor enter和monitor exit
    • volatile使用机器指令lock
    1. 对有序性的保证
    • volatile禁止重排序,但后者可能会发生指令重排序的情况
    1. 线程是否阻塞
    • volatile不会使线程陷入阻塞,而后者会发生这种情况

    相关文章

      网友评论

        本文标题:深入理解volatile

        本文链接:https://www.haomeiwen.com/subject/xegzcctx.html