Java内存模型(二)
volatile型变量的特殊规则
volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义成volatile后,它将具备两种特性,第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程改变了这个变量的值后,新值对于其他线程来说是可以立即得知的;第二个语义是禁止指令重排序,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。
开发人员经常误解的一个描述:“volatile变量对于所有线程是立即可见的,对volatile变量的写操作都能立刻反应到其他线程之中,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的”。这句话的论据部分并没有错,但是其论据不能得出“基于volatile变量的运算在并发下是安全的”这个结论。volatile型变量在各个线程的工作内存中不存在一致性问题,但是Java里面的运算并非原子运算,导致volatile变量的运算在并发下一样是不安全的。 象如下代码:
1 2 |
|
我们知道race++这个操作并不是原子运算,它会被编译成多条字节码指令,故此多线程计算时会出现结果不符合预期的情况。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然需要通过加锁来保证原子性:
- 1) 运算结果并不依赖变量的当前值,或者能够确保只有单一线程修改变量的值
- 2) 变量不需要与其他的状态变量共同参与不变约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
上述代码是一段伪代码,其中描述的场景十分常见,只是我们在处理配置文件时一般不会出现并发而已。如果定义initialized变量时没有使用volatile修饰,就可能由于指令重排序的优化,导致位于线程A中最后一句的代码”initialized=true”被提前执行,这样在线程B中使用配置信息的代码可能出现错误,而volatile关键字则可避免此类情况的发生。
再看看Java内存模型中对volatile变量定义的特殊规则,假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read,load,use,assign,store和write操作时需要满足以下规则:
- 1) 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T对变量V执行load动作。线程T对变量V的use动作可以认为是与线程T对变量V的load和read动作相关联的,必须一起连续出现。(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)
- 2) 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T对变量V执行assign动作。线程T对变量V的assign动作可以认为是与线程T对变量V的store和write动作相关联的,必须一起连续出现。(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改)
- 3) 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是与A相关联的load或store动作,假定动作P是与动作F相应的对变量V的read或write动作;类似地,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是与B相关联的load或store动作,假定动作Q是与动作G相应的对变量V的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)
原子性、可见性和有序性
1) 原子性
由Java内存模型直接保证的原子性变量操作包括read,load,use,assign,store,write这6个,我们大致可以认为基本数据类型的访问读写是具备原子性的(long和double除外)。
如果需要更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,对应的字节码指令是monitorenter和monitorexit,对应到Java代码当中就是synchronized,但是至于moniorenter和monitorexit指令如何对应到lock和unlock操作的请参考《Java虚拟机规范》以及《Java语言规范》。
2) 可见性
可见性就是当一个线程修改了变量共享变量的值,其他线程能够立即得知这个修改。
volatile保证了多线程操作时变量的可见性。
synchronized和final也能实现可见性,同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把“this”的引用传递出去,那么在其他线程中就能看见final字段的值。(this引用逃逸是一件很危险的事情,其它线程可能访问到初始化了“初始化了一半”的对象)
3) 有序性
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的,如果在一个线程观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。
先行发生原则(Happen-before)
先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A所产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法等。
Java内存模型中“天然的”先行发生关系:
1) 程序次序规则(Program Order Rule)
在同一个线程内,程序代码里写在前面的操作先行发生于写在后面的代码。准确地说,因该是控制流顺序而不是程序代码顺序,因为要考虑分支,循环等结构。
2) 管程锁定规则(Monitor Lock Rule)
对某个锁的unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,这里的“后面”是指时间上的先后顺序。也就是说,如果某个锁已经被lock了,那么只有它被unlock之后,其他线程才能lock该锁。表现在代码上,如果是某个同步方法,如果某个线程已经进入了该同步方法,只有当这个线程退出了该同步方法(unlock操作),别的线程才可以进入该同步方法。
3) volatile变量规则(Volatile Variable Rule)
对一个volatile变量的写操作先行发生于对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。也就是说,某个线程对volatile变量写入某个值后,能立即被其它线程读取到。
4) 线程启动规则(Thread Start Rule)
Thread对象的start方法先行发生于此线程的每个动作。
5) 线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测到线程是否已经终止运行。
6) 线程中断规则(Thread Interruption Rule)
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
7) 对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
8) 传递性(Transitivity)
如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
其中程序次序规则,管程锁定规则,volatile变量规则,传递性规则经常用来推断先行发生关系。
需要注意的是,先行发生关系和时间上的先后顺序基本没有太大的关系。
时间上的先后顺序不能得出先行发生关系,示例代码如下所示:
1 2 3 4 5 6 7 |
|
假设存在线程A和线程B,线程A先(时间上的先后)调用了”setValue(1)”,然后线程B调用了同一个对象的”getValue()”,那么线程B收到的返回值是不确定的,由于工作内存和主内存同步存在延迟,也由于可能存在重排序现象。 虽然时间上线程A的setValue()操作先于线程B的getValue()操作,但是并不能推断出线程A的setValue()操作先行发生于线程B的getValue()操作,如果有这种先行发生关系,那么可以推断出线程B的getValue()操作获得的值。
如果我们给getValue()方法和setValue()方法添加synchronized关键字,就能利用管程锁定规则推断出线程A的setValue操作先行发生于线程B的getValue操作,或者我们也可以将value定义为volatile变量,也能利用volatile变量规则推断出先行发生关系。
先行发生关系也不能推断出时间上的先后执行顺序,示例代码如下所示:
1 2 |
|
根据程序次序规则,我们可以推断出”int i=1”的操作先行发生于”int j=2”的操作,但是”int j=2”的代码完全有可能先被处理器执行(时间上的先后),这就是重排序,虚拟机规范是允许这种特性存在的,虚拟机可利用这种特性提高性能。
重排序
在同一个线程操作是顺序执行的(其实是按控制流执行),但是在某个线程看别的线程里的操作,则是乱序执行的。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
这段代码的执行结果是什么呢?我们先不考虑工作内存和主内存的同步问题,假设一种执行顺序,one线程先执行完,然后other线程再执行,那么执行的操作序列会是a=1, x=b, b=1, y=a; 此时的输出结果将是(0,1) 假设另一种执行顺序,other线程先执行,one线程后执行,那么执行的操作序列是b=1,y=a,a=1,x=b,此时的输出结果是(1,0),如果one线程和other线程交替执行,a=1,b=1,y=a,x=b,此时的输错结果是(1,1)。
但是让意外的是竟然还会存在这样一种输出结果(0,0),这种结果很难想象。这便是重排序导致的现象。一种可能的重排序后的执行顺序如下图所示:
线程A执行时本来应该先执行a=1,后执行x=b的,但是由于重排序的原因x=b先执行,a=1后执行。
这一节说的的先后是指时间上的先后,根据内存模型的程序次序规则,线程A里a=1还是先行发生于x=b的。
参考资料
《深入理解Java虚拟机》 第12章 Java内存模型与线程
《Java多线程设计模式》 附录B Java的内存模型。
转载 http://www.cloudchou.com/softdesign/post-637.html
转载于:https://www.cnblogs.com/chenying99/p/4265978.html
Java内存模型(二)相关推荐
- 简单说说你对Java内存模型的原子性的了解吧?
为什么80%的码农都做不了架构师?>>> Java内存模型保证non-long.non-double类型外的其他任意类型的访问都是原子性的.同时volatile long.vo ...
- 并发系列(二)----Java内存模型
一 简介 在并发编程中,两个线程(A.B)同时操作一个普通变量的时候会出现线程A在操作变量时线程B也将变量操作了,此时线程A是无法感知变量发生变化的,造成变量改变错误.更据以上例子我们需要解决的问题就 ...
- 深入理解Java虚拟机(周志明第三版)- 第十二章:Java内存模型与线程
系列文章目录 第一章: 走近Java 第二章: Java内存区域与内存溢出异常 第三章: Java垃圾收集器与内存分配策略 并发处理的广泛应用是Amdahl定律代替摩尔定律成为计算机性能发展源动力的根 ...
- java多线程学习二、安全与不安全示例:12306买票和银行取钱、java内存模型、内存可见性、线程同步块和方法
文章目录 前言 1. 什么是块,分为几种 2. 静态块与构造块的区别 一. 举例说明:并发情况下,线程不安全 1. 示例1:unsafe12306取票 2. 示例2:unsafe银行取钱 二.线程不安 ...
- 深入理解Java虚拟机——第十二章——Java内存模型与线程
硬件效率与一致性 处理器需要与内存交互,但处理器运算速度与对内存的I/O操作速度相差几个数量级,因此现代操作系统不得不加入尽可能接近处理器运算速度的高速缓存来作为内存与处理器之前的缓冲.这样处理器就不 ...
- Java面试题十二:Java内存模型
前言 Java开发人员并不需要像C/C++开发人员,需要时刻注意内存的分配和释放,而是全权交给虚拟机(JVM)去管理,自然关于内存管理或是内存的模型.结构对Java开发来说就是一个"黑箱&q ...
- 深入理解JVM虚拟机读书笔记【第十二章】Java内存模型与线程
12.1 概述 12.2 硬件的效率与一致性 12.3 Java内存模型 12.3.1 主内存与工作内存 12.3.2 内存间交互操作 12.3.3 对于volatile型变量的特殊规则 12.3.4 ...
- java并发编程实战:第十六章----Java内存模型
一.什么是内存模型,为什么要使用它 如果缺少同步,那么将会有许多因素使得线程无法立即甚至永远看到一个线程的操作结果 编译器把变量保存在本地寄存器而不是内存中 编译器中生成的指令顺序,可以与源代码中的顺 ...
- Java内存模型与线程
一.一致性 高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但也存在缓存一致性(cache coherence)问题 二.java内存模型 内存模型:对特定的内存或高速缓存进行读写访问的过程抽象 ...
最新文章
- 2016年ICT产业趋势预测
- C#数组和集合专题4(Hashtable类)
- 计算机系统基础:计算机性能评价知识笔记
- 【九度oj 1135】【OpenJ_Bailian - 2915】 字符串排序 (水)
- 【报告分享】2020中国电商直播粉丝价值研究报告.pdf(附下载链接)
- 新来的领导把我的职务免掉了,一年后,我要不要找领导聊聊?
- 框架基础:ajax设计方案(一)---集成核心请求
- 易语言js加密解密教程
- Flexsim——初学AGV必看的知识点(如何解决AGV在不同区域speed不同)
- 快速入门一个简单的情感分类项目
- DIV+CSS+JavaScript技术制作网页(旅游主题网页设计与制作)云南大理 (1)
- python小游戏 走迷宫小游戏设计与实现
- 哪些赛道适合程序员创业?
- 计算机c盘满了怎么移到d盘去,Win10电脑c盘满了怎么转移到d盘?
- ATF 安全启动过程
- 家长叫我别天天我在房间没事多看看新闻,我说我马上写个爬虫爬新闻看!!!
- [开发过程]<c#上位机>[01]体验MAUI跨平台效果
- Soul(一) Soul基础知识
- easymock 图片_用easymock来mock数据
- Scratch(二十九):虚拟现实交互
热门文章
- 数据的四大特征_大数据
- CF GYM 100703G Game of numbers
- [译] 如何用ps制作泼水字
- SQL2005 安装时 “性能监视器计数器要求(错误)” 解决方案
- python文本进度条代码解释_python动态文本进度条的实例代码
- 深入理解 gRPC 协议--理解protobuf/.proto/http2
- 2021-06-01 深入分析锁的基础知识
- Nginx之虚拟服务器配置
- (32)FPGA面试技能提升篇(EMMC)
- 电脑没有ps怎么改照片dpi_设计干货整理丨 平面设计师必知的打印常识与电脑性能...