美文网首页
Volatile关键字解读

Volatile关键字解读

作者: DoubleFooker | 来源:发表于2019-09-26 09:46 被阅读0次

    并发的由来

    • JAVA内存模型规定了所有变量第一存储在主内存中,每个线程都有自己的工作内存。
    • 每个线程都保存该线程用到的变量的主内存副本,线程操作的都是自己工作内存,而不是主内存
    • 线程访问变量过程为从主内存拷贝一份到工作内存,操作变量不会马上同步到主内存,由JMM控制
    • 线程间无法直接操作其他线程的工作内存,共享变量的传递需要靠工作内存与主内存传递的方式同步

    volatile关键字

    保证可见性,不保证原子性
    通过javap -v 查看可知,JVM通过添加Lock汇编指令实现可见性

    可见性原理

    • CPU、内存、IO处理速度不一致
    • JMM

    由于处理速度的不一致,为了缓解这个问题,引入了CPU高速缓存处理内存处理IO速度的差异,将内存中的数据复制到高速缓存中。多核处理器中会出现情况,CPU0、CPU1缓存了内存数据,CPU0修改了数据同步到缓存中,CPU1还未同步到缓存中,出现缓存不一致,而缓存不一致导致数据操作会有问题。CPU层面为了解决这个问题,引入了总线锁(串行读取,性能降低)和缓存锁。
    缓存锁则利用缓存一致性协议(MESI)实现。对缓存的数据标记状态(Modify修改,Invalid失效,shared一致,exclusive独占,只有一个cpu有值),cpu通过嗅探协议得到其他cpu缓存数据的状态,实现数据的一致性。
    但是MESI是指在硬件层面上解决缓存一致性问题,cpu在发起数据修改时需要通知其他cpu并等待响应,于是优化引入storebuffer实现异步响应提高处理能力。
    而引入storebuffer就带来了可见性问题。并且CPU执行还可能出现重排序问题。


    image.png

    为了解决这个问题,引入内存屏障。

    内存屏障

    变量写的过程包括三个指令:
    load:从主内存读取数据到工作内存
    update:修改变量值
    store:刷新到主内存
    这个过程如果线程A读取了变量I值(10),自增i++(11),此时线程B也读取了变量I值(10),也自增了I++(11)写入主内存,此时线程A强制刷新变量I为B的I++(11)的值,但是工作内存中的I还是11,然后刷新到主内存,导致结果不是想要的(12)。内存屏障前的指令是可以被从排序的,导致线程安全问题。内存屏障的作用是强制刷新到主内存,禁止指令重排序。
    内存屏障的作用:

    • 当修改一个变量时,保证把值刷新到主内存
    • 其他线程用到修改的变量,会把自己工作内存中的变量标记为无效,从主内存读取。
    • 使用场景:对于一个变量只有一个线程会对变量进行写操作,其他线程都是读操作,则可以使用volatile修饰。

    JMM

    • 语言级别的抽象内存模型
    • 控制如何做数据同步及什么时候做数据同步
    • 核心解决了可见性、有序性
    • 通过调用CPU只能实现一致性

    JAVA代码的执行顺序经过编译器、CPU的重排序,出现一些不可预知的执行顺序。
    指令重排序的规则

    • 操作系统、JVM编译器为了优化程序处理的效率,会对操作指令进行调度,重新排序。但必须遵守以下规则
    • 存在依赖关系的指令不重排
    • 不影响单线程的执行结果

    volatile原理

    JVM的volatile使用内存屏障实现
    内存屏障的作用

    • 确保指令重排序时,屏障前面的指令执行完,保证其后的指令不会重排到屏障之前,不会把屏障之前的指令重排序到屏障之后
    • 强制刷新工作内存缓存到主内存;
    • 工作内存变量的写操作会导致其他线程的缓存失效(MESI),其他线程的读操作从主内存读

    JVM中提供了四种内存屏障指令

    • loadload
    • storestore
    • loadstore
    • storeload

    volatile通过storeload(编译器级别的),JVM会调用操作系统底层的指令实现内存屏障来实现的禁止指令重排序。

    局限性

    只保证可见性,不保证原子性。适合只有一个线程修改变量,其他线程读取变量的场景。

    happen-before原则实现可见性

    • 程序的运行顺序规则

    例如代码,在单线程环境下,1 happen-before 2,3 happen-before 4

    public class App {
        private static int x = 0;
        public static volatile boolean isfinish = false;
    
        public void write() {
            x = 1; // 1
            isfinish = true; //2
        }
    
        public void read() {
            if (!isfinish) { // 3
                int a = x; // 4
            }
    
        }
    
    
    • volatile规则

    对于上面的代码因为isfinish加了volatile修饰,则2 happen-before 3

    • 传递性规则

    因为1 hp 2 ,2 hp 3,则传递性规则保证 1 hp 3

    • start规则

    线程启动前的命令必然 hp 线程运行执行的命令(操作同一个变量)

    • join原则

    线程运行执行的代码必然 hp thread.join()命令。join实现原理是wait/notify,通过阻塞主线程,执行完成notifyAll在唤醒继续执行。

    • 锁规则

    前一个线程释放锁必然 hp 后一个线程加锁操作。

    相关文章

      网友评论

          本文标题:Volatile关键字解读

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