Volatile关键字实现原理

@TOC

voliate简介

在上一篇文章中我们深入理解了java关键字synchronized,我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下。
通过上一篇的文章我们了解到synchronized是阻塞同步的,在线程竞争激烈的情况下会升级为重量级锁。而voliate就可以说是java虚拟机提供的最轻量级的同步锁。但它同时不容易被正确理解,也至于在并发编程中有很多程序员遇到线程安全的问题就会使用sychronized。Java内存模型告诉我们,各个线程会将共享变量从主内存拷贝到工作内存,然后执行引擎会基于工作内容中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对于普通变量是没有规定的,而针对voliate修饰的变量给java虚拟机特殊的约定,线程对voliate变量的修改会立即被其他线程感知。

机器硬件CPU与JMM

  1. CPU Cache模型
    为了解决CPU的高速性和内存读取的低效性,在CPU和内存之间加入了Cache缓存,平衡了二者之间的差距,为了更高效更平滑的读取数据,引入了一级缓存、二级缓存、三级缓存。


    上图为程序的局部执行原理,其中内存中的数据为共享数据。每一次CPU修改内存中的数据,需要进行如下操作:
    1. 从内存中读取Cache中
    2. 在Cache中更新数据
    3. 把更新的数据刷新到内存汇总
      上述操作存在CPU缓存不一致的问题。
  2. 一致性问题解决方案
    1. 总线加锁(粒度太大)
      对数据总线、地址总线、控制总线进行加锁,造成效率低下,加锁粒度太大。
    2. MESI
      • 读操作:不做任何事情,把Cache中的数据读到寄存器
      • 写操作:发出信号通知其他的CPU将该变量的Cache line置为无效,其他的CPU要访问这个变量的时候,只能从内存中获取,而不能从Cache中去读。
      • CPU的cache中会增加很多的Cache line
    3. Java内存模型
    • 主存中的数据所有线程都可以访问(共享数据)
    • 每个线程都有自己的工作空间,(本地内存)(私有数据)
    • 工作空间数据:局部变量、内存的副本
    • 线程不能直接修改内存中的数据,只能读到工作空间来修改,修改完成后刷新到内存

Volatile关键字的语义分析

volatile作用:让其他线程能够马上感知到某一线程多某个变量的修改

  • 保证可见性

  • 对共享变量的修改,其他的线程马上能感知到

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.leehao;

    import java.util.concurrent.TimeUnit;

    public class ReaderAndUpdater {

    final static int MAX=50;
    static int init_value=0;

    public static void main(String[] args) {
    new Thread(()->{
    int localValue=0;
    while(localValue<MAX){
    if(localValue!=init_value){
    System.out.println("Reader:"+init_value);
    localValue=init_value;
    }
    }
    },"Reader").start();
    new Thread(()->{
    int localValue=0;
    while(localValue<MAX){
    System.out.println("updater:"+(++localValue));
    init_value=localValue;
    try {
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    },"Updater").start();
    }
    }
    JAVA

    上述程序中,2个线程中的init_value是互相不可见的,所以程序Reader永远无法监听到Updater中的变化,从而输出reader:XXX。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    updater:1
    Reader:1
    updater:2
    updater:3
    updater:4
    updater:5
    updater:6
    updater:7
    updater:8
    updater:9
    updater:10
    updater:11
    APACHE

    如过加入volidate修饰符,那么只要init_value有变化,那么对reader可见,reader对init_value进行监听到,输出如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    updater:1
    Reader:1
    updater:2
    Reader:2
    updater:3
    Reader:3
    updater:4
    Reader:4
    updater:5
    Reader:5
    updater:6
    Reader:6
    updater:7
    Reader:7
    updater:8
    Reader:8
    ...
    ...
    AVRASM

    但不能保证原子性,读、写、(i++)

  • 保证有序性
    重排序(编译阶段、指令优化阶段)
    输入程序的代码顺序并不是实际执行的顺序
    重排序后对单线程没有影响,对多线程有影响
    Volatile
    Happens-before
    volatile规则:
    对于volatile修饰的变量:
    (1)volatile之前的代码不能调整到他的后面
    (2)volatile之后的代码不能调整到他的前面(as if seria)
    (3)霸道(位置不变化)

    1
    2
    3
    4
    5
    6
    7
    int i=0;
    int a=2;
    int b=3;
    volatile int j=3;
    int c=2;
    int d=3;
    int e=c+d;
    JAVA

    上述代码限定,在重排序的时候,volatile前面的代码必须在volatile int j=3;之前执行,volatile之后的代码必须在volatile int j=3;之后执行。但前面的代码和后面的代码内部可以进行重排序。

  • volatile的原理和实现机制(锁、轻量级)
    HSDIS反编译:
    编译顺序Java –class—JVM—ASM文件(会变文件)
    volatile int a ;编译成会变文件后:Lock :a

注: synchronize在1.5之前为重量级锁,1.6之后为偏向锁、轻量级锁。但是偏向锁和轻量级锁需要进行配置,如果不配置,默认还是重量级锁。

Volatile的使用场景

状态标志(开关模式)

started启动默认为false,当started为true的时候,执行一系列的操作。当某个线程在shutdown()中可以将started设置为false,那么所有的线程操作关闭。
伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ShutDowsnDemmo extends Thread{
private volatile boolean started=false;

@Override
public void run() {
while(started){
dowork();
}
}
public void shutdown(){
started=false;
}
}
JAVA

双重检查锁定(double-checked-locking)

单例模式

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
instance=new Singleton();
}
}
return instance;
}
}
JAVA

volatile与synchronized的区别

  • 使用上的区别
    Volatile只能修饰变量,synchronized只能修饰方法和语句块
  • 对原子性的保证
    synchronized可以保证原子性,volatile不能保证原子性
  • 对可见性的保证
    都可以保证可见性,但实现原理不同
    volatile对变量加了lock,synchronized使用monitorEnter和monitorexit 对应刀JVM中是monitor指令
  • 对有序性的保证
    volatile能保证有序,synchronizedye 可以保证有序性,但是代价太大,会升级到重量级锁,并发退化到串行
  • 其他
    synchronized 会引起线程阻塞
    volatile 不会引起线程阻塞

Volatile关键字实现原理
https://leehoward.cn/2020/04/04/Volatile关键字实现原理/
作者
lihao
发布于
2020年4月4日
许可协议