zoukankan      html  css  js  c++  java
  • Java 中的 volatile 关键字

    Java 中 volatile 关键字是一个类型修饰符。JDK 1.5 之后,对其语义进行了增强。

    • 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了共享变量的值,共享变量修改后的值对其他线程立即可见
    • 通过禁止编译器、CPU 指令重排序和部分 happens-before 规则,解决有序性问题

    volatile 可见性的实现

    • 在生成汇编代码指令时会在 volatile 修饰的共享变量进行写操作的时候会多出 Lock 前缀的指令
    • Lock 前缀的指令会引起 CPU 缓存写回内存
    • 一个 CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效
    • volatile 变量通过缓存一致性协议保证每个线程获得最新值
    • 缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改
    • 当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存

    看一下我们之前的一个可见性问题的测试例子

    package constxiong.concurrency.a014;
    
    /**
     * 测试可见性问题
     * @author ConstXiong
     */
    public class TestVisibility {
    
        //是否停止 变量
        private static boolean stop = false;
        
        public static void main(String[] args) throws InterruptedException {
            //启动线程 1,当 stop 为 true,结束循环
            new Thread(() -> {
                System.out.println("线程 1 正在运行...");
                while (!stop) ;
                System.out.println("线程 1 终止");
            }).start();
            
            //休眠 10 毫秒
            Thread.sleep(10);
            
            //启动线程 2, 设置 stop = true
            new Thread(() -> {
                System.out.println("线程 2 正在运行...");
                stop = true;
                System.out.println("设置 stop 变量为 true.");
            }).start();
        }
        
    }

    程序会一直循环运行下去

    这个就是因为 CPU 缓存导致的可见性导致的问题。

    线程 2 设置 stop 变量为 true,线程 1 在 CPU 1上执行,读取的 CPU 1 缓存中的 stop 变量仍然为 false,线程 1 一直在循环执行。

    示意如图:

    给 stop 变量加上 valatile 关键字修饰就可以解决这个问题。

    volatile 有序性的实现

    • 3 个 happens-before 规则实现:

    1) 对一个 volatile 变量的写 happens-before 任意后续对这个 volatile 变量的读

    2) 在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作

    3) happens-before 传递性,A happens-before B,B happens-before C,则 A happens-before C

    • 内存屏障(Memory Barrier 又称内存栅栏,是一个 CPU 指令)禁止重排序

    1) 在程序运行时,为了提高执行性能,在不改变正确语义的前提下,编译器和 CPU 会对指令序列进行重排序。

    2) Java 编译器会在生成指令时,为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的指令重排序

    3) 编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令

    4) 内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序

    内存屏障

    • 为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的 CPU 重排序。
    • 对于编译器,内存屏障将限制它所能做的重排序优化;对于 CPU,内存屏障将会导致缓存的刷新操作
    • volatile 变量的写操作,在变量的前面和后面分别插入内存屏障;volatile 变量的读操作是在后面插入两个内存屏障

    1) 在每个 volatile 写操作的前面插入一个 StoreStore 屏障
    2) 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障
    3) 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
    4) 在每个 volatile 读操作的后面插入一个 LoadStore 屏障

    • 屏障说明

    1) StoreStore:禁止之前的普通写和之后的 volatile 写重排序;
    2) StoreLoad:禁止之前的 volatile 写与之后的 volatile 读/写重排序
    3) LoadLoad:禁止之后所有的普通读操作和之前的 volatile 读重排序
    4) LoadStore:禁止之后所有的普通写操作和之前的 volatile 读重排序

    我觉得,有序性最经典的例子就是 JDK 并发包中的显式锁 java.util.concurrent.locks.Lock 的实现类对有序性的保障。

    以下摘自:Java锁是如何保证数据可见性的

    实现 Lock 的代码思路简化为

    private volatile int state;
    
    void lock() {
        read state
        if (can get lock)
            write state
    }
    
    void unlock() {
        write state
    }
    • 假设线程 a 通过调用lock方法获取到锁,此时线程 b 也调用了 lock() 方法,因为 a 尚未释放锁,b 只能等待。
    • a 在获取锁的过程中会先读 state,再写 state。
    • 当 a 释放掉锁并唤醒 b,b 会尝试获取锁,也会先读 state,再写 state。

    Happens-before 规则:一个 volatile 变量的写操作发生在这个 volatile 变量随后的读操作之前。


     来一道刷了进BAT的面试题?

  • 相关阅读:
    java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header.
    spring-session-data-redis依赖冲突问题
    centos7启动iptables时报Job for iptables.service failed because the control process exited with error cod
    图片上传后台服务报内存溢出 Out Of Memory Java heap space
    mysql 数据库密码忘记重置 进行远程连接
    打Jar包
    Type interface com.innovationV2.mapper.UserMapper is not known to the MapperRegistry
    关于java基础类型Integer String的clone()
    clion使用clang编译
    token & refresh token 机制总结
  • 原文地址:https://www.cnblogs.com/ConstXiong/p/11687758.html
Copyright © 2011-2022 走看看