volatile关键字作用与内存可见性、指令重排序概述[JAVA]「建议收藏」

volatile关键字作用与内存可见性、指令重排序概述[JAVA]「建议收藏」在理解volotile关键字的作用之前,先粗略解释下内存可见性与指令重排序。1.内存可见性Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其JVM内存模型大

大家好,又见面了,我是你们的朋友全栈君。

在理解 volotile 关键字的作用之前,先粗略解释下内存可见性与指令重排序。

1. 内存可见性

Java 内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其 JVM 模型大致如下图。

这里写图片描述
JVM 模型规定:1) 线程对共享变量的所有操作必须在自己的内存中进行,不能直接从主内存中读写; 2) 不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。这样的规定可能导致得到后果是:线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。这就引出了内存可见性。

内存可见性指:当一个线程修改了某个状态对象后,其它线程能够看到发生的状态变化。比如线程 1 修改了变量 A 的值,线程 2 能立即读取到变量 A 的最新值,否则线程 2 如果读取到的是一个过期的值,也许会带来一些意想不到的后果。那么如果要保证内存可见性,必须得保证以下两点:

  1. 线程修改后的共享变量值能够及时刷新从工作内存中刷新回主内存;
  2. 其它线程能够及时的把共享变量的值从主内存中更新到自己的工作内存中;

为此,Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其它线程。当把共享变量声明为 volatile 类型后,线程对该变量修改时会将该变量的值立即刷新回主内存,同时会使其它线程中缓存的该变量无效,从而其它线程在读取该值时会从主内中重新读取该值(参考缓存一致性)。因此在读取 volatile 类型的变量时总是会返回最新写入的值。

除了使用 volatile 关键字来保证内存可见性之外,使用 synchronizer 或其它加锁也能保证变量的内存可见性。只是相比而言使用 volatile 关键字开销更小,但是 volatile 并不能保证原子性,大致原理如下:

JAVA内存模型规定工作内存与主内存之间的交互协议,其中包括8种原子操作:

  1. lock:将主内存中的变量锁定,为一个线程所独占
  2. unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量
  3. read:将主内存中的变量值读到工作内存当中
  4. load:将read读取的值保存到工作内存中的变量副本中。
  5. use:将值传递给线程的代码执行引擎
  6. assign:将执行引擎处理返回的值重新赋值给变量副本
  7. store:将变量副本的值存储到主内存中。
  8. write:将store存储的值写入到主内存的共享变量当中。

其中lock和unlock定义了一个线程访问一次共享内存的界限,而其它操作下线程的工作内存与主内存的交互大致如下图所示。

这里写图片描述

从上图可以看出,read and load 主要是将主内存中数据复制到工作内存中,use and assign 则主要是使用数据,并将改变后的值写入到工作内存,store and write 则是用工作内存数据刷新主存相关内容。但是以上的一系列操作并不是原子的,也既是说在 read and load 之后,主内存中变量的值发生了改变,这时再 use and assign 并不是取的最新的值。所以尽管 volatile 会强制工作内存与主内存的缓存更新,但是却仍然无法保证其原子性。

2. 指令重排序

首先看下以下线程A和线程B的部分代码:

线程A:
content = initContent();	//(1)
isInit = true;				//(2)
线程B
while (isInit) {			//(3)
    content.operation();    //(4)
}

从常规的理解来看,上面的代码是不会出问题的,但是JVM可以对它们在不改变数据依赖关系的情况下进行任意排序以提高程序性能(遵循as-if-serial语义,即不管怎么重排序,单线程程序的执行结果不能被改变),而这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不会被编译器和处理器考虑,也即是说对于线程A,代码(1)和代码(2)是不存在数据依赖性的,尽管代码(3)依赖于代码(2)的结果,但是由于代码(2)和代码(3)处于不同的线程之间,所以JVM可以不考虑线程B而对线程A中的代码(1)和代码(2)进行重排序,那么假设线程A中被重排序为如下顺序:

线程A:
isInit = true;				//(2)
content = initContent();	//(1)

对于线程B,则可能在执行代码(4)时,content并没有被初始化,而造成程序错误。那么应该如何保证绝对的代码(2) happens-before 代码(3)呢?没错,仍然可以使用volatile关键字。

volatile关键字除了之前提到的保证变量的内存可见性之外,另外一个重要的作用便是局部阻止重排序的发生,即保证被volatile关键字修饰的变量编译后的顺序与 也即是说如果对isInit使用了volatile关键字修饰,那么在线程A中,就能保证绝对的代码(1) happens-before 代码(2),也便不会出现因为重排序而可能造成的异常。

3. 总结

综上所诉,volatile关键字最主要的作用是:

  1. 保证变量的内存可见性
  2. 局部阻止重排序的发生

4. 附录 – happens-before原则

英文原文:

  • Each action in a thread happens before everyaction in that thread that comes later in the program’s order.
  • An unlock on a monitor happens before everysubsequent lock on that same monitor.
  • A write to a volatile field happens before everysubsequent read of that same volatile.
  • A call to start() on a thread happens before anyactions in the started thread.
  • All actions in a thread happen before any otherthread successfully returns from a join() on that thread.

中文描述:

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens-before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  • Thread.start()的调用会happens-before于启动线程里面的动作。
  • Thread中的所有动作都happens-before于其他线程从Thread.join中成功返回。

5. 参考文献

[1] Brian Goetz.Java并发编程实战.机械工业出版社.2012
[2] http://ifeve.com/easy-happens-before/
[3] http://www.infoq.com/cn/articles/java-memory-model-2/
[4] http://www.cnblogs.com/mengyan/archive/2012/08/22/2651575.html
[5] http://my.oschina.net/chihz/blog/58035
[6] http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html
[7] http://ifeve.com/jvm-reordering/
[8] …

以上仅为个人学习所记笔记,如有错误,欢迎指正

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/138469.html原文链接:https://javaforall.net

(0)
上一篇 2022年5月31日 下午12:16
下一篇 2022年5月31日 下午12:36


相关推荐

  • USB转485/232

    USB转485/232USB转485模块双向传输防浪涌屏蔽线UT-890a/Z-TECUSB转232模块双向传输防浪涌屏蔽线Z-TEC

    2022年5月1日
    40
  • 互联网协议

    互联网协议

    2021年10月10日
    42
  • Linux rsync命令

    Linux rsync命令root xuexi rsync etc fstab tmp 在本地同步 root xuexi rsync r etc172 16 10 5 tmp 将本地 etc 目录拷贝到远程主机的 tmp 下 以保证远程 tmp 目录和本地 etc 保持同步 root xuexi rsync r172 16 10 5

    2026年3月26日
    2
  • idea maven创建springboot项目_springboot项目

    idea maven创建springboot项目_springboot项目前言:如今springboot越来越火,越来越多的公司选择使用springboot作为项目的开发框架,其设计目的就是用来简化spring项目的搭建和开发过程,省略了传统spring、springmvc项目繁琐的配置,可以让开发人员快速上手。下面详细说明下如何使用idea创建我们的第一个springboot项目:首先打开idea主界面选择CreateNewProject在弹…

    2022年10月13日
    4
  • int什么数据类型_SQL基本数据类型

    int什么数据类型_SQL基本数据类型 一开始看到Int16,Int32,Int64这三种类型就觉得有点怪,为什么要整个数字结尾的,这么干就是想让大家一眼就知道这个数据类型占多大空间吧.Int16,等于short,占2个字节.-3276832767Int32,等于int,占4个字节.-21474836482147483647Int64,等于long,占8个字节.-9223372036854…

    2022年8月15日
    5
  • Linux网络下载管理工具(lftp, ftp, lftpget, wget)「建议收藏」

    Linux网络下载管理工具(lftp, ftp, lftpget, wget)「建议收藏」网络客户端管理工具在Linux中,通常用网络客户端管理工具实现文件的下载与上传,主要有以下几种,分别为lftp工具,ftp工具,lftpget工具,wget工具,在centos7中,要尽量学会lftp,lftpget等工具,下面多这些工具的简单使用逐一介。lftp使用命令manlftp可查看其具体的使用方法,如果lftp工具未安装,使用yuminstalllftp命令进…

    2022年5月29日
    44

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号