【java多线程系列】java中的volatile的内存语义

时间:2022-06-04 23:48:45

在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 关系的图形化表现形式如下:

【java多线程系列】java中的volatile的内存语义

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

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

二volatile写-读的内存语义

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

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

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

【java多线程系列】java中的volatile的内存语义

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

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

【java多线程系列】java中的volatile的内存语义

从图上可以看到,在读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多线程系列--&OpenCurlyDoubleQuote;基础篇”03之 Thread中start&lpar;&rpar;和run&lpar;&rpar;的区别

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

  8. Java多线程系列--&OpenCurlyDoubleQuote;JUC锁”03之 公平锁&lpar;一&rpar;

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

  9. Java多线程系列--&OpenCurlyDoubleQuote;JUC锁”04之 公平锁&lpar;二&rpar;

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

随机推荐

  1. &lpar;转&rpar;ORA-12514 TNS 监听程序当前无法识别连接描述符中请求服务 的解决方法

    早上同事用PL/SQL连接虚拟机中的Oracle数据库,发现又报了"ORA-12514 TNS 监听程序当前无法识别连接描述符中请求服务"错误,帮其解决后,发现很多人遇到过这样的问 ...

  2. JavaScript实现TwoQueues缓存模型

    本文所指TwoQueues缓存模型,是说数据在内存中的缓存模型. 无论何种语言,都可能需要把一部分数据放在内存中,避免重复运算.读取.最常见的场景就是JQuery选择器,有些Dom元素的选取是非常耗时 ...

  3. 在实现和使用上与select和poll有很大差异

    在看此课程的读者,希望先阅读关于函数基础内容 函数定义与函数作用域 的章节,因为此课程或多或少会涉及函数基础的内容,而基础内容,本人放在 函数定义函数作用域 章节. 本文直接赘述函数参数与闭包,若涉及 ...

  4. 自定义UIAlertView

    You can change accessoryView to any own customContentView in a standard alert view in iOS7 [alertVie ...

  5. Struts2中s&colon;set标签和s&colon;if标签小结

    1.  s:set标签 格式:<s:set name="" value="" scope=””/> 说明:把jsp页面中的一个值,以name存储起来 ...

  6. POJ2201&plus;RMQ

    /* RMQ */ #include<stdio.h> #include<string.h> #include<stdlib.h> #include<algo ...

  7. Web前端-Vue&period;js必备框架(五)

    Web前端-Vue.js必备框架(五) 页面组件,商品列表组件,详情组件,购物车清单组件,结算页组件,订单详情组件,订单列表组件. vue-router 路由 vuex 组件集中管理 webpack ...

  8. 参数签名ascii码排序的坑

    参数签名中通常是按键值对中键名称的ASCII按从小到大的顺序排序后进行hash为签名字符串.不要直接使用 SortedDictionary<string, string> 有坑的,他是按数 ...

  9. spring&period;net框架配置和使用

    spring.net框架学习笔记 spring.net框架是用于解决企业应用开发的复杂性的一种容器框架,它的一大功能IOC(控制反转),通俗解释就是通过spring.net框架的容器创建对象实体,而不 ...

  10. 2017春季阿里大文娱&lpar;优酷&rpar;——C&plus;&plus;研发一面

    一.C++基础 1.1 sizeof 问题(空类.含虚函数.内存对齐) 1.2类构造的时候会默认生成哪些函数,C++11多了什么?(move,左\右值) 1.3为什么c++不类似java一样实现一个内 ...