volatile关键字使用场所

简介: volatile关键字使用场所

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

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

volatile 可见性的实现

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

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

package com.wityx.www.a014;
/**
 * 测试可见性问题
 * //http://www.wityx.com/post/659_1_1.html
 * @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 有序性的实现

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

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

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

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;
//http://www.wityx.com/post/659_1_1.html
void lock() {
    read state
    if (can get lock)
        write state
}
 
void unlock() {
    write state
}

//http://www.wityx.com/post/659_1_1.html

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

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

相关文章
|
1月前
|
存储 缓存 安全
打工人,从 JMM 透析 volatile 与 synchronized 原理
打工人,从 JMM 透析 volatile 与 synchronized 原理
57 2
|
14天前
|
Java
volatile关键字的作用
volatile关键字的作用
5 0
|
29天前
|
安全 Java 编译器
Java多线程基础-6:线程安全问题及解决措施,synchronized关键字与volatile关键字(一)
线程安全问题是多线程编程中最典型的一类问题之一。如果多线程环境下代码运行的结果是符合我们预期的,即该结果正是在单线程环境中应该出现的结果,则说这个程序是线程安全的。 通俗来说,线程不安全指的就是某一代码在多线程环境下执行会出现bug,而在单线程环境下执行就不会。线程安全问题本质上是由于线程之间的调度顺序的不确定性,正是这样的不确定性,给我们的代码带来了很多“变数”。 本文将对Java多线程编程中,线程安全问题展开详细的讲解。
42 0
|
11月前
|
缓存
|
Java
关于关键字volatile的一二
关于关键字volatile的一二
56 0
|
存储 安全 Java
漫画:什么是volatile关键字?(整合版)
Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
漫画:什么是volatile关键字?(整合版)
|
安全 Java Spring
重点丨什么是双重检查锁模式?以及为何需要 volatile 关键字?
双重检查锁定(Double check locked)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量。这个模式还可以用来创建单例。下面来看一个 Spring 中双重检查锁定的例子。
重点丨什么是双重检查锁模式?以及为何需要 volatile 关键字?