在java的多线程编程中,synchronized和volatile都扮演着重要的 角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性,可见性指的是当一个线程修改一个共享变量时,另一个线程能够读到这个修改后的值。如果volatile修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。本文将从volatile的JMM内存语义的角度带领大家全面认识volatile修饰符。

一volatile的特性

可见性:对volatile变量的读总是能看到任意线程对这个volatile变量最后的写入,即当一个线程修改了volatile变量的值,新值对于其他线程而言是可以立即得知的。而对于普通共享变量是做不到这点的,在前面的中讲过,为了提高处理速度,处理器不直接和内存打交道,而是先将内存数据读取到缓存中然后再进行操作,但操作之后不知道何时写回内存。

因此普通变量不能够做到一个线程修改了其值,新值对于其他线程而言是可以立即得知的,那么volatile是如何做到的呢?

这是因为含volatile修饰符的java代码在转换为汇编代码的时候会在代码中插入一个包含Lock前缀的指令代码,这个指令会做以下两件事:

1.将当前处理器缓存行的数据写回到系统内存。

2.这个写回内存的操作会使得其他CPU中缓存了该内存地址的数据无效。

那么为何添加这两个条件之后就可以做到新值对于其他线程而言是可以立即得知的呢?还是接着上面的过程分析,上面说到对于普通变量读取到缓存之后,不知道何时写回内存,而如果用volatile修饰的话,在进行写操作的时候,JVM会向处理器发送一条Lock前缀的指令,将这个变量缓存中的内容写回到系统内存,但是可能其它处理器在它写回内存之前就已经更新自己缓存中的数据,那么这样的话,仍然和上面一样不能保证结果的正确性,因此添加了第二条,即为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,即每个处理器会检查自己缓存中的数据是否过期,如果过期,则会将缓存行内容设置为无效,当处理器对这个缓存行数据进行操作的时候就会从系统内存读取数据,而Lock指令的第二条就是使其缓存无效,因此即使其它处理器在处理器A将缓存中的内容重新写回系统内存之前就读取了系统内存中的值,仍然可以保证其它处理器在使用自己缓存中的数据之前从内存中再读取一次数据(因为Lock指令的第二条使得其缓存中的值无效),这样就相当于线程A修改了volatile变量的值,其新值对于其他线程而言是可以立即得知的。(虽然这个过程仍然与普通变量一样要通过系统内存来实现,但在效果上或者说内存语义上与“新值对于其他线程而言是可以立即得知的”效果一样)



原子性:对任意单个volatile变量的读/写具有原子性,但类似volatile++这种复合操作不具备原子性。

对于volatile的原子性可以理解为对volatile变量的单个的读/写可以看做是使用同一个锁对这些单个的读/写操作作了同步操作。因为它们之间的执行效果是相同的。

二volatile写-读建立的happens-before关系

在前面的volatile的特性的第一条可见性我们解释了为何“一个线程修改了volatile变量的值,新值对于其他线程而言是可以立即得知的”,但是上面是在处理器实现的原理上进行分析的,事实上通过JMM内存模型定义的规则也可以推出上述结论。这就是volatile写-读建立的happens-before关系

从内存语义的角度来看,volatile的写-读与锁的释放-获取具备相同的内存效果:即volatile写和锁的释放内存语义相同,volatile读与锁的获取内存语义相同。而我们知道对于同一块内存的锁必须先释放后其它线程才可以获取,即一个线程释放锁一定在其它线程获取锁之前。因此一个线程对volatile的写肯定happens-before其它线程对volatiel的读。

下面我们基于上述特性来看一下volatile写-读建立的happens-before关系,代码如下:

class VolatileExample {
int a = 0;
volatile boolean flag = false; public void writer() {
a = 1; //1
flag = true; //2
} public void reader() {
if (flag) { //3
int i = a; //4
……
}
}
}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before 关系可以分为两类:





根据程序次序规则,1 happens before 2; 3 happens before 4。

根据volatile规则,2 happens before 3。

根据happens before 的传递性规则,1 happens before 4。

上述happens before 关系的图形化表现形式如下:

根据图示可以很容易的推出1 happens-before 4,即线程A修改了缓存中的共享变量,在线程B读取同一个共享变量时,读到的将是线程A修改之后的值(即最终写回主存中的值),相当于该共享变量立即对线程B可见。

这个图比前面的那段文字叙述理解起来容易的多,但那段文字叙述才是JMM实现的原理解释,希望读者能认真体会。

二volatile写-读的内存语义

关于volatiel写-读的内存语义,前面也提到过,这里再详细讲解一下:

volatile写的内存语义:写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。

以上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:

可以看到线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。即volatile写的内存语义保证了本地内存与主存一致性。

volatile读的内存语义:读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

从图上可以看到,在读flag变量后,本地内存B已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值也变成一致的了。即volatile读的内存语义保证了读线程中本地内存与主存的一致性。

正是因为上述写的一致性与读的一致性保证了最终读的线程读取到的一定是写线程刷新后的值,从而保证内存的可见性。

以上就是本博客的主要内容,重点理解volatile的特性以及volatile写-读建立的happens-before关系是如何保证内存一致性的。

如果读者觉得本博客写的不错,记得小手一抖,点个赞哦!另外欢迎大家关注我的博客账号哦,将会不定期的为大家分享技术干货,福利多多哦!

【java多线程系列】java中的volatile的内存语义的更多相关文章

  1. java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析

    java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...

  2. (Java 多线程系列)java volatile详解

    在前面的文章里面介绍了synchronized关键字的用法,这篇主要介绍volatile关键字的用法. Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其它 ...

  3. (Java 多线程系列)java synchronized详解

    synchronized简介 Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block).同步代码块包括两部分:一个作为锁对象的引用,一个作为由这个锁保护的代码块. ...

  4. (Java 多线程系列)Java 线程池(Executor)

    线程池简介 线程池是指管理同一组同构工作线程的资源池,线程池是与工作队列(Work Queue)密切相关的,其中在工作队列中保存了所有等待执行的任务.工作线程(Worker Thread)的任务很简单 ...

  5. Java多线程系列——从菜鸟到入门

    持续更新系列. 参考自Java多线程系列目录(共43篇).<Java并发编程实战>.<实战Java高并发程序设计>.<Java并发编程的艺术>. 基础 Java多线 ...

  6. java多线程系列 目录

    Java多线程系列1 线程创建以及状态切换    Java多线程系列2 线程常见方法介绍    Java多线程系列3 synchronized 关键词    Java多线程系列4 线程交互(wait和 ...

  7. Java多线程系列--“基础篇”03之 Thread中start()和run()的区别

    概要 Thread类包含start()和run()方法,它们的区别是什么?本章将对此作出解答.本章内容包括:start() 和 run()的区别说明start() 和 run()的区别示例start( ...

  8. Java多线程系列--“JUC锁”03之 公平锁(一)

    概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...

  9. Java多线程系列--“JUC锁”04之 公平锁(二)

    概要 前面一章,我们学习了“公平锁”获取锁的详细流程:这里,我们再来看看“公平锁”释放锁的过程.内容包括:参考代码释放公平锁(基于JDK1.7.0_40) “公平锁”的获取过程请参考“Java多线程系 ...

随机推荐

  1. ●BZOJ 2555 SubString

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=2555题解: 后缀自动机+LCT 不难发现,对于输入的询问串,在自动机里trans后的到的状态 ...

  2. ●BZOJ 2820 YY的GCD

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=2820 题解: 莫比乌斯反演 先看看这个题:HDU 1695 GCD(本题简化版) HDU 1 ...

  3. 51nod 1035:最长的循环节

    1035 最长的循环节 基准时间限制:1 秒 空间限制:131072 KB 分值: 20 难度:3级算法题   正整数k的倒数1/k,写为10进制的小数如果为无限循环小数,则存在一个循环节,求< ...

  4. bzoj3309DZY Loves Math

    3309: DZY Loves Math Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 1240  Solved: 777[Submit][Statu ...

  5. day5 liaoxuefeng---访问数据库、web开发、异步IO

    一.访问数据库 二.web开发 三.异步IO

  6. .net带参数SQL语句的完整定义

    首先是在DAL数据访问层中的代码://数据更新的方法public static int shuxing_update(s_passnature model) { string sql = " ...

  7. 浅谈JAVA8引入的接口默认方法

    参考 http://blog.csdn.net/wanghao_0206/article/details/52712736 public interface InterfaceTest { publi ...

  8. axios的兼容性处理

    一.简介 看看官网的简介: "Promise based HTTP client for the browser and node.js" 译:基于 Promise 的 HTTP ...

  9. find 命令查找文件,文件夹

    查找文件 find / -name httpd.conf 查找文件夹 find / -name "*1526*" -type d, 其中双引号里的东西表示文件夹名字包含" ...

  10. iOS开发-文件管理

    iOS学习笔记(十七)--文件操作(NSFileManager) 浅析 RunLoop 解决EXC_BAD_ACCESS错误的一种方法--NSZombieEnabled iOS开发--Swift篇&a ...