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. mac和linux下使用Docker,部署SpringBoot项目到docker

    主要是看一下如何在linux及mac上安装docker,创建docker镜像,部署SpringBoot项目到docker,并借助于DaoCloud进行docker镜像下载加速等. 我用的电脑是mac, ...

  2. kali视频(16-20)学习

    第五周 kali视频(16-20)学习 16.漏洞分析之数据库评估(一) 17.漏洞分析之数据库评估(二) 18.漏洞分析之WEB应用代理 19.漏洞分析之burpsuite 20.漏洞分析之fuzz ...

  3. kali视频(1-5)

    第二周 kali视频(1-5) 1.kali安装 2.基本配置 vmtools安装过程. 3.安全渗透测试一般流程 4.信息搜集之GoogleHack 5.信息搜集之目标获取 1.kali安装 直接在 ...

  4. 用python写定时任务

    一个是sched模块,一个是threading模块 参考链接:http://www.cnblogs.com/LinTeX9527/p/6181523.html

  5. JAMstack 最佳实践

    摘自官方介绍,没有翻译(没必要,已经比较简单了,重要的就是进行每条的诠释了,后续...) Entire Project on a CDN Because JAMstack projects don’t ...

  6. 【HTML5】Canvas绘图详解-1

    ----->Canvas绘制基础 1,线条绘制 1-1,线条组成的图形和beginPath 案例:绘制由不同颜色的线条组成的图案 1-2,多边形的填充和closePath 案例:绘制封闭具有填充 ...

  7. (转)Android代码混淆-添加了Gson遇到的问题

    折腾了好久.....郁闷 -_- 1.首先,project.properties里的配置文件变了,之前的项目一直都是在project.properties这个文件中添加一行proguard.confi ...

  8. (转)setTextColor()的参数设置方式

    setTextColor()的参数设置方式 分类: Android界面研究2011-12-09 23:27 11160人阅读 评论(2) 收藏 举报 查了下资料发现setTextColor()的参数应 ...

  9. flask 之 mongodb

    查看mongod 是否启动,启动了会显示进程ID和程序名 pgrep mongod -l 查找mongod的位置whereis mongod 或locate mongod 启动mongodmongod ...

  10. ubuntu下安装ros出现“无法下载-package.ros.org中某个包-校验和不符”的解决方法

    新安装的Ubuntu14.04,为了科研马上准备装ros indigo,却困难重重,一步一个坎. 比如说按照ros wiki里面一步一步来,当运行sudoapt-get update 然后出现下列情况 ...