问题  :

  • volatile 解决的是什么问题
  • 有什么应用场景

概述

某些共享变量的时候我们使用volatile 修饰,它会保证修改的值立即被更新到主存,或是从主存中获取最新的值。它的底层是如何实现的?

volatile 使用场景

通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换,但是使用volatile必须满足两个条件:
1、对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果准确性的;
2、该变量没有包含在具有其它变量的不变式中。(出处

下面列出两个例子,都使用了volatile

1. 状态标记量

public class ServerHandler {
private volatile isopen;
public void run() {
if (isopen) {
//促销逻辑
} else {
//正常逻辑
}
}
public void setIsopen(boolean isopen) {
this.isopen = isopen
}
}

多个线程下,为使得isopen 这个值最新,我们使用了 volatile .

2、double check

class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

这个单例模式下,例如A线程正在初始化对象,但是没初始化好(局部成员没初始化好),此时B 线程来了,此时判断对象已经不为空了,那么就返回了一个错误的对象。这个时候可以使用volatile .

class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

如何保证内存可见性?

下面这段描述来自 : https://www.jianshu.com/p/195ae7c77afe ,非原创

在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据,下面看看操作普通变量和volatile变量有什么不同:

1、对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。

2、对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。

volatile变量的内存可见性是基于内存屏障(Memory Barrier)实现的,什么是内存屏障?内存屏障,又称内存栅栏,是一个CPU指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM(Java Memory Modal)为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。这个地方我们回想一下上面的单利模式下的 double-check ,正是由于内存不可见性导致了错误。

下面的章节我们将会介绍原理。

volatile 原理

了解volatile原理前,我们需要了解重排序。

重排序

编译器为了快速地完成编译工作,优化了代码顺序,即是说有些代码本来在前的有可能在后编译,相反也是有可能的。

public class VolatileTest {

    int a = 0;
int b = 0; public void set() {
a = 1;
b = 1;
} public void loop() {
while (b == 0) continue;
if (a == 1) {
System.out.println("i'm here");
} else {
System.out.println("what's wrong");
}
}
}

两线程分别执行set 和loop ,结果会是怎么样?不一定,这里涉及到了编译器的重排序和CPU的重排序。CPU 的重排序是怎么回事呢?可以先看一下下面图 :

可以看到 CPU 和 L1之前存在LoadBuff 和 StoreBuffer ,它们可以认为是又多一级缓存,具体的工作如下

1、CPU执行load读数据时,把读请求放到LoadBuffer,这样就不用等待其它CPU响应,先进行下面操作,稍后再处理这个读请求的结果。
       2、CPU执行store写数据时,把数据写到StoreBuffer中,待到某个适合的时间点,把StoreBuffer的数据刷到主存中。

因为StoreBuffer的存在,CPU在写数据时,真实数据并不会立即表现到内存中,所以对于其它CPU是不可见的;同样的道理,LoadBuffer中的请求也无法拿到其它CPU设置的最新数据;

由于StoreBuffer和LoadBuffer是异步执行的,所以在外面看来,先写后读,还是先读后写,没有严格的固定顺序。

所以由于CPU的更新不同步导致有可能读到过时的信息。

源码解析

有如下代码

public class VolatileTest {
static volatile int num;
public static void main(String[] args) {
num = 5;
}
}

再使用 javap –v 指令,看一下编译的字节码。

我们看到了比平时多了一个标志 : ACC_VOLATILE ,而putstatic和不加volatile 的情况是一样的,那么我们知道在使用volatile后会去获取最新的,那么真实的实现逻辑会不会在 putstatic 这上面呢?

下面也是根据狼哥的文章,自己找到执行方法的地方。下面我们来看看这一句到底执行了什么

bytecodeInterpreter.cpp 文件下,

我们主要看这三个地方,首先判断 cache –> is_volatile ,是不是volatile 修饰的,然后进入release_int_field_put 方法

,我们看一下这个方法 ,

再调用 release_store

遇到我们看到了使用 volatile 的形参,这里的volatile 是C++ ,逻辑就是赋值而已,那么C++ 的volatile 的作用是什么呢?

c/c++中的volatile关键字,用来修饰变量,通常用于语言级别的 memory barrier,在"The C++ Programming Language"中,对volatile的描述如下:

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

紧接着执行OrderAccess::storeload(),这又是啥?

其实这就是经常会念叨的内存屏障,之前只知道念,却不知道是如何实现的。从CPU缓存结构分析中已经知道:一个load操作需要进入LoadBuffer,然后再去内存加载;一个store操作需要进入StoreBuffer,然后再写入缓存,这两个操作都是异步的,会导致不正确的指令重排序,所以在JVM中定义了一系列的内存屏障来指定指令的执行顺序。

JVM中定义的内存屏障如下,JDK1.7的实现

总结

-     volatile 只保证了“可见性”,不能保证原子性,典型的例子就是 i++ ,只保证了下一次读和写都将会去内存中拿最新的值

-     volatile 可以避免编译器的重排序,底层的实现是JVM 制定的内存屏障

-     volatile 相对于锁有良好的性能

参考资料

java 基础 --- volatile的更多相关文章

  1. Java基础 - volatile

    volatile的作用:对与volatile修饰的变量, 1,保证该变量对所有线程的可见性. 2,禁止指令重排序. Java内存模型(JMM) 原子性 i = 2; 把i加载到工作内存副本i,副本i= ...

  2. Java基础教程:多线程杂谈——双重检查锁与Volatile

    Java基础教程:多线程杂谈——双重检查锁与Volatile 双重检查锁 有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化.此时程序员可能会采用延迟初始化.但要正确实 ...

  3. [Java面经]干货整理, Java面试题(覆盖Java基础,Java高级,JavaEE,数据库,设计模式等)

    如若转载请注明出处: http://www.cnblogs.com/wang-meng/p/5898837.html   谢谢.上一篇发了一个找工作的面经, 找工作不宜, 希望这一篇的内容能够帮助到大 ...

  4. 最适合作为Java基础面试题之Singleton模式

    看似只是最简单的一种设计模式,可细细挖掘,static.synchronized.volatile关键字.内部类.对象克隆.序列化.枚举类型.反射和类加载机制等基础却又不易理解透彻的Java知识纷纷呼 ...

  5. Java基础知识【下】( 转载)

    http://blog.csdn.net/silentbalanceyh/article/details/4608360 (最终还是决定重新写一份Java基础相关的内容,原来因为在写这一个章节的时候没 ...

  6. Java基础加强之多线程篇(线程创建与终止、互斥、通信、本地变量)

    线程创建与终止 线程创建 Thread类与Runnable接口的关系 public interface Runnable { public abstract void run(); } public ...

  7. Java基础常见英语词汇

    Java基础常见英语词汇(共70个) ['ɔbdʒekt] ['ɔ:rientid]导向的                             ['prəʊɡræmɪŋ]编程 OO: object ...

  8. java 基础题 很基础, 很有趣

    都是一些非常非常基础的题,是我最近参加各大IT公司笔试后靠记忆记下来的,经过整理献给与我一样参加各大IT校园招聘的同学们,纯考Java基础功底, 老手们就不用进来了,免得笑话我们这些未出校门的孩纸们, ...

  9. JAVA基础知识(转)

    本文就java基础部分容易混淆的一些知识点进行了一下总结.因为Java本身知识点非常多,不可能在很短的篇幅就能叙述完,而且就某一个点来讲,如欲仔细去探究,也能阐述的非常多.这里不做全面仔细的论述,仅做 ...

随机推荐

  1. Nmap 扫描

    最近在家里学习渗透,看到了nmap对服务器进行端口扫描,记录学习如下: Nmap支持非常多的扫描方式,包括TCP Syn ,TCP Connect,TCP ACK,TCP FIN/Xmas/NULL, ...

  2. 【文文殿下】快速傅里叶变换(FFT)学习笔记

    多项式 定义 形如\(A(x)=\sum_{i=0}^{n-1} a_i x^i\)的式子称为多项式. 我们把\(n\)称为该多项式的次数界. 显然,一个\(n-1\)次多项式的次数界为\(n\). ...

  3. bzoj3435 [Wc2014]紫荆花之恋(动态点分治+替罪羊树)

    传送门(权限) 传送门(非权限) 题解 我终终终终终终于做出来啦!!! 作为一个没有学过替罪羊树的蒟蒻现场学了一下替罪羊树,作为一个平衡树都写数组版本的看着大佬的指针题解无语只能硬去理解然后照着抄了一 ...

  4. jquery中通过trim() length 判断数据是否有值

    在jquery中可以通过如下方式判断某一个字符串是否有值,结合if else if 语句进行业务逻辑的处理 <!DOCTYPE html> <html lang="en&q ...

  5. java----session

    什么是session? 在WEB开发中,服务器可以为每个用户浏览器创建一个会话对象(session对象),也就是说他是保存在服务端的.注意:一个浏览器独占一个session对象(默认情况下).因此,在 ...

  6. ansible api2.0 多进程执行不同的playbook

    自动化运维工具:ansible 多进程调用ansible api的应用场景:   应用系统检查 一个应用系统可能具有20—50台服务器的集群,初步的系统层面检查可以用一个统一的playbook来检查, ...

  7. 类型转换 / BOOL 类型

    /* Swift不允许隐式类型转换, 但可以使用显示类型转换(强制类型转换) OC: int intValue = 10; double doubleValue = (double)intValue; ...

  8. datetime之 utcnow 和now的用法

    from datetime import datetime print(datetime.now()) 本地时间 print(datetime.utcnow()) 国际时间

  9. (二)Audio子系统之new AudioRecord()

    在上一篇文章<(一)Audio子系统之AudioRecord.getMinBufferSize>中已经介绍了AudioRecord如何获取最小缓冲区大小,接下来,继续分析AudioReco ...

  10. jedis 整合 Spring

    一,   单机版整合spring 1. 使用默认配置文件进行配置 <bean id="redisClient" class="redis.clients.jedis ...