Volatile浅析

volatile关键字介绍

volatile关键字只能修饰类变量和实例变量,对于方法参数,局部变量以及实例常量,类常量多不能进行修饰。不如下面代码中MAX变量就无法使用volatile进行修饰。

我们先来看一个简单的程序

public class VolatileFoo {    final static int MAX = 5;    static int value = 0;    public static void main(String[] args){        new Thread(() -> {            int localValue = value;            while(localValue < MAX) {                if(value != localValue) {                    System.out.println("The value is updated to" + value);                    localValue = value;                }            }        }).start();        new Thread(() -> {            int localValue = value;            while(localValue < MAX) {                System.out.println("The value well be changed to" + ++localValue);                value = localValue;                try{                    // 短暂休眠,为使上一个线程能过输出变化内容                    TimeUnit.SECONDS.sleep(1);                } catch (InterruptedException ignored) {                }            }        }).start();    }}

我们可以先想以下运行结果。时Thread-1更新一次,Thread-0就能够及时输出呢?下面我们来看下运行结果

The value is updated to 1The value well be changed to 2The value well be changed to 3The value well be changed to 4The value well be changed to 5

我们可以看到Thread-0线程几乎没有感知到Thread-1对value造成的变化,从而陷入了死循环,为什么呢?我们对代码坐下小小的调整:

static volatile int value = 0;

在value变量增加volatile关键字进行修饰,再来看下运行结果,你会发现Thread-0可以感知到Thread-1对value做出的修改。

The value is updated to 1The value well be changed to 2The value is updated to 2The value well be changed to 3The value is updated to 3The value well be changed to 4The value is updated to 4The value well be changed to 5The value is updated to 5

为什么会这样呢?我们来看下面的内容。

CPU缓存一致性问题

在针对缓存一致性问题的协议中最为出名的时Intel的MESI协议,MESI协议保证了每一个缓存中使用的共享变量副本多是一致的,他的大概思想是,当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是说在其他的CPU Cache中也存在一个副本,那么就进行如下操作:

​ 1、读取操作:不做任何处理,只是将Cache中的数据读取到寄存器中
​ 2、写入操作:发出信号通知其他CPU将该变量的Cache line设置为无效。其他CPU在对该变量进行操作的时候,将从主内存中再次获取。

Java的内存模型

Java内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主存之间的抽象关系,具体如下:

  1. 共享变量存储于内存之中,每个线程都可以访问。
  2. 每个线程都是私有的工作内存或者成为本地内存。
  3. 工作内存只存储该线程对共享变量的副本
  4. 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
  5. 工作内存和Java内存模型一样也是一个抽象的概念,他其实并不是真实存在的,它涵盖了缓存、寄存器、编译优化以及硬件等。

假设主存有个共享变量X为0,线程1和线程2分别拥有共享变量X的副本,线程1此时将工作内存的X修改为1,同时刷到了主存中,当线程2想要去使用工作内存X的副本时,就会发现该变量副本已经失效了,必须到主存中重新去获取新的X的副本,存储在自己的工作内存中。这一点和CPU和CPU Cache之间的关系非常相似。

并发的三大特性以及JVM如何保证三大特性

1、原子性
a、多个原子性的操作在一起就不再具备原子性操作了
b、简单的读取与赋值操作是原子性的,将一个变量赋值给另一个变量的操作不是原子性的
c、Java内存模型只保证了基本读取和赋值的原子性操作,其他的均不保证,如果想要是的某些代码具备原子性,需要使用关键字Synchronized或者JUC中的lock。
总结:volatile不具备原子性

2、有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,在单线程的情况下,重排序并不会引起什么问题,但是在多线程模式下,重排序会影响程序的正常运行。Java提供了三种保证有些性的方式,具体如下:
a、使用volatile可以保证有序性
b、使用synchronized关键字来保证有序性
c、使用显示锁Lock来保证有序性
后两者采用同步机制来保证有序性。volatile保证有序性是直接禁止JVM和处理器对volatile关键字修饰的指令进行重排序,但是对于volatile前后无依赖的指令则无影响。

3、可见性

​ 在多线程的环境下,如果某个线程读取共享变量,则首先获取主存中该变量,然后存入工作内存中。以后工作就只需操作工作内存中的变量副本即可。同样如果对改变了执行了修改操作,则先将新值写入工作内存中然后再刷入主存中。但是什么时候最新值会被刷入主存中是不确定的。这也就解释了我们刚开始提到了那个代码Thread-1修改线程后Thread-0无法获取到value的最新变化了。

Java提供了三种方式来保证可见性:
a、使用volatile关键字,当一个变量被volatile关键字修饰时,对于共享资源的读操作会直接在内存中进行。对于共享资源的写操作也是先修改工作内存,但是修改完后会立即刷新到主内存中。
b、通过synchronized关键字能够保证可见性
c、通过JUC提供的显示锁Lock也能够保证可见性。

volatile的原理和实现机制

可以在OpenJDK下的unsafe.cpp源码中可以发现,被volatile修饰的变量存在一个“lock;”的前缀,源码如下:
```C++
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

……

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
asm volatile (LOCK_IF_MP(%4) "cmpxchg1 %1,(%3)"
: "=a" (exchage_value)
: "r" (exchage_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchage_value;
}

"lock;"前缀相当于是一个内存屏障,该内存屏障会为指令的执行提供如下几个保障:    1、确保指令重排是不会将其后面的代码重排到内存屏障之前和之后    2、确保指令在执行到内存屏障修饰的指令是前面的代码全部执行完毕    3、强制将线程工作内存中的修改的值刷新至主内存中    4、如果是写操作,则会导致其他的线程工作内存中的缓存数据失效## 补充:volatile 和 synchronized区别1、使用的区别    a、volatile关键字只能用于修饰实变量或类变量,不能用于修饰方法以及方法参数、局部变量和常量等。    b、synchronized关键字不能对变量进行修饰,只能用于修饰方法和语句块    c、volatile修饰的变量可以为null,synchronized同步语句块的monitor对象不能为空2、对原子性的保证    a、volatile不能保证原子性    b、由于synchronized是一种排他的锁机制,因此被synchronized修饰的同步代码块是无法被中途打断的,一次可以保证代码的原子性。3、对可见性的保证    a、两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不相同。    b、synchronized借助与JVM指令monitor enter 和 monitor exit通过排他的方式使得同步代码串行化,在monitor exit是所有共享资源的会被刷新到主内存中去。    c、volatile使用机器指令“lock;"的方式迫使其他线程工作内存中的数据失效。4、对有序性的保证    a、volatile关键字禁止JVM编译器以及处理器对代码进行重排序,所以他能够保证有序性。    b、synchronized是通过排他禁止实现同步代码块前后代码的有序性,而在同步代码块里面的代码无法保证其有序性。5、其他    a、volatile不会进入阻塞状态    b、synchronized会是线程进入阻塞状态。
©著作权归作者所有:来自51CTO博客作者有间猫呀的原创作品,如需转载,请注明出处,否则将追究法律责任

更多相关文章

  1. JavaScript中的预解析(变量提升)介绍!
  2. 如何处理Linux服务器内存过高?
  3. 干货丨手把手教你如何加载和操作DolphinDB内存分区表
  4. C语言中static 试题
  5. 深入理解 JVM 的 GC overhead limit exceeded 错误!
  6. Volatile原理概述
  7. PHP 共享内存使用场景及注意点
  8. Redis 内存为什么不宜过大
  9. YUM变量缺失导致的问题小记

随机推荐

  1. 详解之php反序列化
  2. 教你用php将二维码和文字结合到一个背景
  3. 两分钟带你了解PHP中的运算符
  4. 2021最常用的8个代码编辑器推荐
  5. PHP方法处理微信昵称特殊符号过滤
  6. 推荐给初学者必看的PHP书籍
  7. php上传图片无法显示的问题
  8. 详解php中整数判断的方法(附代码)
  9. php中的绘图技术详解
  10. PHP如何实现支付宝支付功能(图文详解)