volatile、Synchronized实现变量可见性的原理,volatile使用注意事项
变量不可见的两个原因
Java每个线程工作都有一个工作空间,需要的变量都是从主存中加载进来的。Java内存模型如下(JMM):

线程访问一个共享的变量时,都需要先从主存中加载一个副本到自己的工作内存中,经过自己修改后再更新到主存中去。在这个过程中可能出现这种情况:线程A在工作内存中修改了变量1的值,但是还没有写入主存,这档口线程B将变量1加载到自己工作内存中。显然,线程B拿到的不是变量1的最新值了。
变量可见性就是: 这个变量被任何一个线程修改了,其他线程都能“看见”,也就是能取到变量最新的值。
重排序是指为了适合cpu指令执行机制,编译器、内存系统、处理器可能会对一些指令的执行顺序进行重排。例如:
int a = 1; //line1
int b = 2; //line2
int s = a*b; //line3
line1 和 line2 可能会被重排颠倒位置,但是line3不会重排,因为Java单线程下会遵守 as-if-serial语义,简单的讲就是重排指令不会出现错误的结果。在多线程下,指令重排则可能造成一些问题,例如:
class Example {
int a = 0;
boolean flag = false;
public void writer() {
    a = 1;
    flag = true;
}
public void reader() {
    if (flag) {
        int i =  a +1;
    }
}
}
线程A首先执行writer()方法,线程B线程接着执行reader()方法。线程B在int i=a+1 时不一定能看到a已经被赋值为1,因为在writer()中,两句话顺序可能打乱:
线程A执行顺序: flag=true;(a=1;还没执行完或还没写到主存)
线程B执行:flag=true (而此时a=0) 产生了一些与我们预期之外的情况。
导致变量不可见的原因(1)更新不及时,(2)多线程交替执行时的指令重排序。
volatile实现可见性的原理
JVM线程工作时的原子性指令有:
read: 从主存读取一个变量的值的副本到线程的工作内存。
load:read读来的值,把这个值填充到线程使用的变量中,然后就可以使用了。
use:要使用一个变量,先发出这个指令。
assign:赋值,给变量一个新值。
write:将变量的新值运送到主存中。
store:将写到主存的新值存到那个变量中。
上述操作必定是顺序执行的,但可不一定连续,中间可能插入其他指令。为了保证可见性:关键就是保证load、use的执行顺序不被打乱(保证使用变量前一定进行了load操作,从主存拿最新值来),assign、wirte的执行顺序不被打乱(保证赋值后马上就是把值写到主存)。
所以使用内存屏障, CPU指令,可以禁止指令执行乱序:插入一个内存屏障, 相当于告诉CPU和编译器指令顺序先于这个指令的必须先执行,后于这个命令的必须后执行。
解决第一个导致不可见的因素(更新不及时):内存屏障,对于volatile修饰的变量,读操作时在读指令use之前插入一条读屏障指令重新从主存加载最新值进来,保证了load、use指令的执行顺序不乱;写操作时在写指令assign之后插入一条写屏障指令,将工作内存变量的最新值立刻写入主存变量。
解决第二个因素(指令重排): 由于读写数据时会在之前/后插入一条内存屏障指令,因此volatile可以禁止指令重排序。
Synchronized实现可见性原理
解决第一个因素:在加锁前会将工作内存的值全部重新加载一遍,保证最新;释放锁前将工作内存的值全部更新到主存;由于在带锁期间,没有其他线程能访问本线程正在使用的共享变量,这样就保证了可见性。
解决第二个因素: 由于Synchronized修饰的代码块都是原子性执行的,即一旦开始做,就会一直执行完毕,期间有其他线程不可以访问本线程所使用的共享变量,这样,即便指令重排了也不会出现问题。
volatile不具有原子性可能导致的问题
经过前面的总结,可以看出Synchronized包裹的代码里的共享变量有可见性、指令不可排序性、原子性;volatile修饰的变量有可见性、指令不可排序性;volatile并不保证变量有原子性。如果用volatile修饰变量希望保证它的原子性就可能出现问题,例如:
volatile int num = 0; 线程A:
num++; 线程B:
num++;
两个线程操作都进行num++的操作,理论上完事后 num 的值为2,但是num++ 的操作本身不是原子性的(包括读取 num原先的值、+1、把+1后的值写入num),volatile也不能使对num的操作变为原子性。因此可能有num = 1的结果:
线程A:读取了num的值,为0,然后阻塞了。
线程B:读取num的值,还是为0,+1后立即写入主存,num = 1(体现可见性)。
线程A:恢复执行了,已经做完读取操作,工作内存中num = 0,继续执行。num+1,写入内存,num = 1;
虽然这种情况发生的概率很小,但是并发量大时还是会出现不少这种情况的。(可以用Synchronized、lock、AtomicInteger等解决)
由于原子性的问题,volatile不适合修饰依赖自身的变量 : num++、a = a*2... 也不适合修饰不变式的变量(即volatile变量应该是独立的):a<b....
volatile、Synchronized实现变量可见性的原理,volatile使用注意事项的更多相关文章
- 使用 volatile 关键字保证变量可见性和禁止指令重排序
		
volatile 概述 volatile 是 Java 提供的一种轻量级的同步机制.相比于传统的 synchronize,虽然 volatile 能实现的同步性要差一些,但开销更低,因为它不会引起频繁 ...
 - volatile关键字与内存可见性
		
前言 首先,我们使用多线程的目的在于提高程序的效率,但是如果使用不当,不仅不能提高效率,反而会使程序的性能更低,因为多线程涉及到线程之间的调度.CPU上下文的切换以及包括线程的创建.销毁和同步等等,开 ...
 - java 轻量级同步volatile关键字简介与可见性有序性与synchronized区别 多线程中篇(十二)
		
概念 JMM规范解决了线程安全的问题,主要三个方面:原子性.可见性.有序性,借助于synchronized关键字体现,可以有效地保障线程安全(前提是你正确运用) 之前说过,这三个特性并不一定需要全部同 ...
 - volatile和synchronized实现内存可见性的区别
		
先看看synchronized实现内存可见性 加锁(synchronized同步)的功能不仅仅局限于互斥行为,同时还存在另外一个重要的方面:内存可见性.我们不仅希望防止某个线程正在使用对象状态而另一个 ...
 - volatile关键字与内存可见性&原子变量与CAS算法
		
1 .volatile 关键字:当多个线程进行操作共享数据时, 可以保证内存中的数据可见 2 .原子变量:jdk1.5后java.util.concurrent.atomic 包下提供常用的原子变量 ...
 - jvm高级特性(5)(1)(原子性,可见性,有序性,volatile,概述)
		
JVM高级特性与实践(十二):高效并发时的内外存交互.三大特征(原子.可见.有序性) 与 volatile型变量特殊规则 简介: 阿姆达尔定律(Amdahl):该定律通过系统中并行化与串行化的比重来描 ...
 - JMM内存模型+volatile+synchronized+lock
		
硬件内存模型: Java内存模型: 每个线程都有一个工作内存,线程只可以修改自己工作内存中的数据,然后再同步回主内存,主内存由多个内存共享. 下面 8 个操作都是原子的,不可再分的: 1) lock ...
 - 【JUC系列第一篇】-Volatile关键字及内存可见性
		
作者:毕来生 微信:878799579 什么是JUC? JUC全称 java.util.concurrent 是在并发编程中很常用的实用工具类 2.Volatile关键字 1.如果一个变量被volat ...
 - Volatile如何保证线程可见性之总线锁、缓存一致性协议
		
基础知识回顾 下图给出了假想机的基本设计.中央处理单元(CPU)是进行算术和逻辑操作的部件,包含了有限数量的存储位置--寄存器(register),一个高频时钟.一个控制单元和一个算术逻辑单元. 时钟 ...
 
随机推荐
- git rebase 理解
			
摘录自:https://blog.csdn.net/wangnan9279/article/details/79287631
 - vscode vue开发环境搭建
			
以前仅了解过VUE但没有真正上手过,现在因为工作需要准备再近几个月里系统的学习一下这款超火的前端框架,希望大佬们指教. ---------------------------------------- ...
 - Linux基本操作及安装(部分)
			
1.分别用cat \tac\nl三个命令查看文件/etc/ssh/sshd_config文件中的内容, 并用自己的话总计出这三个文档操作命令的不同之处? [root@localhost ~]# c ...
 - 为什么阿里Java规约禁止使用Java内置线程池?
			
IDEA导入阿里规约插件,当你这样写代码时,插件就会自动监测出来,并给你红线提醒. 告诉你手动创建线程池,效果会更好. 在探秘原因之前我们要先了解一下线程池 ThreadPoolExecutor 都有 ...
 - Wtm携手LayUI -- .netcore 开源生态我们是认真的!
			
经过WTM团队和LayUI团队多次深入协商,双方于2019年7月29日在北京中国国际展览中心正式达成战略合作意向, 双方签署了战略合作框架协议,LayUI团队承诺使用WTM框架的任何项目都可以免费使用 ...
 - Oracle  存储过程批量插入数据
			
oracle 存储过程批量插入大量数据 declare numCount number; userName varchar2(512); email varchar2(512); markCommen ...
 - centos开发环境安装
			
执行 yum install gcc gcc-c++ gcc-g77 flex bison autoconf automake bzip2-devel zlib-devel ncurses-devel ...
 - Go中的反射reflect
			
前面我们在学习到struct结构体的时候,因为结构体中的字段首字母大写,而我们想把json文件映射到该结构体上时,需要在在结构体字段后面加上json标签,表明结构体字段和json字段的映射关系.这其中 ...
 - GitHub项目:jkrasnay/sqlbuilder的使用
			
http://www.jianshu.com/p/7f099b8cf5f0 技术选型: 在报表查询时,通常需要做可以动态添加的条件 在老项目中使用的是一种Tcondition的对象分装sql impo ...
 - 洛谷 P2657 [SCOI2009]windy数
			
题意简述 求l~r之间不含前导零且相邻两个数字之差至少为2的正整数的个数 题解思路 数位DP 代码 #include <cstdio> #include <cstring> # ...