volatile是java语言中的关键字,用来修饰会被多线程访问的共享变量,是JVM提供的轻量级的同步机制,相比同步代码块或者重入锁有更好的性能。它主要有两重语义,一是保证多个线程对共享变量访问的可见性,二防止指令重排序。

2.语义一:内存可见性

2.1 一个例子

public class TestVolatile {

    public static void main(String[] args) throws InterruptedException {

        ThreadDemo threadDemo = new ThreadDemo();
new Thread(threadDemo).start();
threadDemo.flag = false;
System.out.println("已将flag置为" + threadDemo.flag); } static class ThreadDemo implements Runnable { boolean flag = true; @Override
public void run() {
System.out.println("Flag=" + flag);
} }
}

当你多次执行代码时,有一定几率会出现这种结果

在主线程将子线程实例的flag置为false后,子线程中的flag竟然还是true。这是怎么回事?这就是多线程的内存可见性问题。对于一个没有volatile修饰的的共享变量,当一个线程对其进行了修改,另一线程并不一定能马上看见这个被修改后的值。为什么会出现这种情况呢?这就要从java的内存模型谈起。

2.2 java的内存模型(JMM)

java的内存模型定义了线程和主内存之间的抽象关系,它的内容主要包括:

  • 主要由多线程共享的主内存和各线程私有的工作内存组成(工作内存是个抽象概念,并不真实存在,是对缓冲区,cpu寄存器等的抽象)
  • 变量都存储于主内存中,但是线程的工作内存中保存着要使用的变量在主内存中的副本。
  • 线程对变量的操作必须在工作内存中进行,不同的线程无法直接访问对方的工作内存,相互通信必须经过主内存。

线程,主内存,工作内存三者的交互关系如图所示

看看JMM模型会给我们在多线程环境下的读写带来什么样的问题。

  • 当一个线程(线程1)对共享变量进行修改时,修改的并不是主内存中的变量,而是该线程对应的工作内存中该变量的一个副本。
  • 当主内存中的变量值已经被修改,另一个线程(线程2)读取的却还是自己工作内存中的旧值。

这时就出现了共享变量在多线程环境下的可见性问题。如果把线程的工作内存当作主内存的缓存,这个问题的本质就在于如何解决缓存失效问题。那么JMM中是如何解决可见性问题的?这就不得不提到happens-before规则。

2.3 happens-before规则

happens-before规则又叫先行发生规则。它定义了java内存模型中两项操作的偏序关系,更确切的说,它定义了操作可见性之间的偏序关系。比如A操作 happens-before B操作,并不意味这A操作一定在B操作之前,而是A操作的影响能被操作B观察到,这个影响包括改变了内存中共享变量的值,发送消息等。那么JMM定义了哪些happens-before规则?

  • 1.程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  • 2.监视器锁规则:对于一个锁的解锁,happens-before于随后对这个锁的加锁。
  • 3.volatile变量规则:对于一个volatile 变量的写,happens-before于任意后续对这个volatile变量的读。

    这里对于我们而言重要的是第三点。即对于一个volatile变量,写操作happens-before于读操作,也就是说,一个线程对volatile变量做了修改,另一个线程能马上读到这个被修改后的值。

    这样就能解决共享变量在多线程环境下的可见性问题了。结合JMM模型,我们可以继续探讨下volatile是如何做到这点的。

2.4 volatile解决内存可见性问题的原理

当一个变量被修饰为volatile后,对其的读写就会显得比较特别

  • 1.写一个volatile变量时,JMM首先修改工作内存中的变量值,并刷新到主内存中

    如图所示

  • 2.读一个变量时,JMM会把该线程对应的本地内存置为无效,并从主内存中读取共享变量。

    如图所示

对volatile变量的读写,可以说都是直接对主内存进行的操作,这样虽然会牺牲一些性能,但是解决了“缓存一致性问题”,使得变量在多线程间的可见性得到了很好的保证。

3. 语义二:禁止指令重排

3.1 为什么会有指令重排

为了优化程序性能,编译器和处理器会对java编译后的字节码和机器指令进行重排序,通俗的说代码的执行顺序和我们在程序中定义的顺序会有些不同,只要不改变单线程环境下的执行结果就行。但是在多线程环境下,这么做却可能出现并发问题。比如下面的例子。

3.2 线程不安全的双重检查单例模式

运行这段代码我们可能会得到一个匪夷所思的结果:我们获得的单例对象是未初始化的。为什么会出现这种情况?因为指令重排。首先要明确一点,同步代码块中的代码也是能够被指令重排的。然后来看问题的关键

 INSTANCE = new Singleton();

虽然在代码中只有一行,编译出的字节码指令可以用如下三行表示

  • 1.为对象分配内存空间
  • 2.初始化对象
  • 3.将INSTANCE变量指向刚分配的内存地址

    由于步骤2,3交换不会改变单线程环境下的执行结果,故而这种重排序是被允许的。也就是我们在初始化对象之前就把INSTANCE变量指向了该对象。而如果这时另一个线程刚好执行到代码所示的2处
if (INSTANCE == null)

那么这时候有意思的事情就发生了:虽然INSTANCE指向了一个未被初始化的对象,但是它确实不为null了,所以这个判断会返回false,之后它将return一个未被初始化的单例对象!整个过程的执行流程如下图所示

由于重排序是编译器和CPU自动进行的,那么有什么办法能禁止这种重排序操作吗?很简单,给

INSTANCE变量加个volatile关键字就行,这样编译器就会根据一定的规则禁止对volatile变量的读写操作重排序了。而编译出的字节码,也会在合适的地方插入内存屏障,比如volatile写操作之前和之后会分别插入一个StoreStore屏障和StoreLoad屏障,禁止CPU对指令的重排序越过这些屏障。

4. volatile的其他特性

对volatile变量的读写具有原子性,但是其他操作并不一定具有原子性,一个简单的例子就是i++。由于该操作并不具有原子性,故而即使该变量被volatile修饰,多线程环境下也不能保证线程安全。

5.总结

volatile是jvm提供的轻量级同步工具。被volatile修饰的共享变量在多线程环境下可以获得可见行保证。其次它还能禁止指令重排。由于对volatile的写-读与锁的释放-获取具有相同的内存语义,故某些时候可以代替锁来获得更好的性能。但是和锁不一样,它不能保证任何时候都是线程安全的。

轻量级的同步机制——volatile语义详解(可见性保证+禁止指令重排)的更多相关文章

  1. jvm运行机制和volatile关键字详解

    参考https://www.cnblogs.com/dolphin0520/p/3920373.html JVM启动流程 1.java虚拟机启动的命令是通过java +xxx(类名,这个类中要有mai ...

  2. Java中Volatile关键字详解

    一.基本概念 先补充一下概念:Java并发中的可见性与原子性 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值, ...

  3. 无锁的同步策略——CAS操作详解

    目录 1. 从乐观锁和悲观锁谈起 2. CAS详解 2.1 CAS指令 2.3 Java中的CAS指令 2.4 CAS结合失败重试机制进行并发控制 3. CAS操作的优势和劣势 3.1 CAS相比独占 ...

  4. Java Volatile 关键字详解

    原文链接:https://www.cnblogs.com/zhengbin/p/5654805.html 一.基本概念 先补充一下概念:Java 内存模型中的可见性.原子性和有序性. 可见性: 可见性 ...

  5. Java中Volatile关键字详解(转载)

    转载自:https://www.cnblogs.com/zhengbin/p/5654805.html 一.基本概念 先补充一下概念:Java 内存模型中的可见性.原子性和有序性. 可见性: 可见性是 ...

  6. Java中Volatile关键字详解 (转自郑州的文武)

    java中volatile关键字的含义:http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html 一.基本概念 先补充一下概念:J ...

  7. Java并发编程:JMM (Java内存模型) 以及与volatile关键字详解

    目录 计算机系统的一致性 Java内存模型 内存模型的3个重要特征 原子性 可见性 有序性 指令重排序 volatile关键字 保证可见性和防止指令重排 不能保证原子性 计算机系统的一致性 在现代计算 ...

  8. 简单的互斥同步方式——synchronized关键字详解

    目录 1. 关于synchronized关键字 2. synchronized的原理和实现细节 2.1 synchronized可以用在那些地方 2.2 synchronized是如何实现线程互斥访问 ...

  9. 大数据学习笔记——Spark工作机制以及API详解

    Spark工作机制以及API详解 本篇文章将会承接上篇关于如何部署Spark分布式集群的博客,会先对RDD编程中常见的API进行一个整理,接着再结合源代码以及注释详细地解读spark的作业提交流程,调 ...

随机推荐

  1. 旧书重温:0day2【8】狙击windows的异常处理实验

    现在进入0day2的第六章内容 其中第六章的书本内容我都拍成了图片格式放在了QQ空间中(博客园一张一传,太慢了)http://user.qzone.qq.com/252738331/photo/V10 ...

  2. I.MX6 PHY fixup 调用流程 hacking

    /********************************************************************************** * I.MX6 PHY fixu ...

  3. JFinal自定义FreeMarker标签

    为什么采用freemarker? 1.模板技术,不依附于语言和框架,前端和后端解耦,便于分工协作,更好的协同. 2.页面相应速度快 3.前端非常的灵活,采用自定义标签可以在不更改后端的基础上很容易的构 ...

  4. gcc编译 汇编 选项

    gcc生成main.out的步骤分解:<blockquote>main.c-----(-S 编译)-------->main.s-------(-c 汇编)------->ma ...

  5. bzoj 2734 集合选数

    Written with StackEdit. Description <集合论与图论>这门课程有一道作业题,要求同学们求出\(\{1, 2, 3, 4, 5\}\)的所有满足以 下条件的 ...

  6. Net Core网络通信

    Net Core网络通信 https://www.cnblogs.com/xxred/p/9859893.html 聊聊如何设计千万级吞吐量的.Net Core网络通信! 作者:大石头 时间:2018 ...

  7. 深入理解vsto,开发word插件的利器

    开发了vsto,客户那边也有一些反映插件安装失败或者加载不上的情况.于是我下定决定再理解下vsto的工作机制,如下图: 如上图所示,我把vsto的解决方案分为两部分,一部分是vsto Add-ins, ...

  8. JAVA如何以追加的方式向文件中写入信息?

    以FileWriter类为例: FileWriter的构造方法中有一个方法是:FileWriter(String fileName, boolean append)  ,其中第二个参数决定了写文件的方 ...

  9. 【spring源码学习】spring的AOP面向切面编程的实现解析

    一:Advice(通知)(1)定义在连接点做什么,为切面增强提供织入接口.在spring aop中主要描述围绕方法调用而注入的切面行为.(2)spring定义了几个时刻织入增强行为的接口  => ...

  10. UVA12296 Pieces and Discs

    题意 PDF 分析 可以看成直线切割多边形,直接维护. 对每个多边形考虑每条边和每个点即可. 时间复杂度?不过\(n,m \leq 20\)这种数据怎么都过了.据说是\(O(n^3)\)的,而且常数也 ...