Java并发关键字Volatile 详解
Java并发关键字Volatile 详解
问题引出:
1.Volatile是什么?
2.Volatile有哪些特性?
3.Volatile每个特性的底层实现原理是什么?
相关内容补充:
缓存一致性协议:MESI
由于计算机储存设备(硬盘等)的读写速度和CPU的计算速度有着几个数量级别的差距,为了不让CPU停下来等待读写,在CPU和存储设备之间加了高速缓存,每个CPU都有自己的高速缓存,而且他们共享同一个主内存区域,当他们都要同步到主内存时,如果每个CPU缓存里的数据都不一样,这时应该以哪个数据为准呢?为了解决这一同步问题,需要各个处理器都遵循一定的协议,比如MSI,MOSI,MESI等,目前用的比较多的就是MESI协议。
注 :缓存一致性协议是在总线上实现的。
MESI是代表了缓存数据的四种状态,分别是Modified、Exclusive、Shared、Invalid:
①M(Modified):被修改的,处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没 有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。
②E(Exclusive):独占的,处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改, 即与内存中一致。
③S(Shared):共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。
④I(Invalid):要么已经不在缓存中,要么它的内容已经过时。为了达到缓存的目的,这种状态 的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它从来没被加载到缓存中。在 缓存行中有这四种状态的基础上,
<font color='red'>总结:</font>每个处理器通过嗅探在总线上传递的数据来检查自己缓存的数据是否过期,当处理器发现自己缓存行数据对应的内存地址被修改,就会将当前缓存行里的数据设置为无效。当再次需要使用该数据的时候就会去主内存中重新读取数据。
Java内存模型:JMM
Java内存模型规定了Java变量(实例字段,静态字段,构成数组对象的元素等,但不包括局部变量和方法参数)存储到内存和从内存中取出的的底层实现细节,这些变量都存储在主(Main Memory)中,
(主内存只是虚拟机内存的一部分)
每个线程都有自己的工作内存(Working Memory),工作内存(实际上工作内存并不存在,他只是JMM抽象出来的一个概念)中保存着从主内存读取来的变量副本拷贝
,线程对副本的操作(读取,修改,赋值)都要在工作内存中进行,而不能直接在主内存中进行,不同线程之间也不能访问彼此的工作内存。且线程之间变量值的传递需要经过主内存作为第三方中介。
内存间原子性交互操作:
①lock(锁定):作用于主内存上的变量,当一个变量被标识为Lock的时候,表示该变量是线程 独占状态,此时其他线程不可以对该变量进行操作。早前的缓存一致性协议就是这样,但是这 样会导致某个变量被一个线程占用,其他线程不可以对其进行访问,并发就变成了串行,效率 降低,在后来的缓存一致性协议中就抛弃了这种做法。
②unlock(解锁):同样是作用于主内存中的变量,使变量从锁定状态释放出来,其他线程才可 以对其操作。
③read(读取):读取主内存中的变量,传输到线程的工作内存,等待后续的load操作。
④load(加载):加载工作内存中的变量,把其放入工作内存的副本变量中。
⑤use(使用):把工作内存中的变量副本值传递给
执行引擎
,每当虚拟机遇到使用变量值字节码 的时候就会进行此操作。⑥assign(赋值):把一个从执行引擎接收到的数据赋给工作内存的变量,即执行赋值操作。
⑦store(存储):把工作内存中经过赋值更新后的值传递到主内存中,为后续write做准备。
⑧write(写入):把store操作传递来的值写入主内存,替换之前的值,完成同步更新。
4.JMM并发的特性要求:
①可见性(Visibility):可见性要求是指当一个线程修改了共享变量的值以后,其他线程能够马 上得知这个修改。
②原子性(Atomicity):原子性是指对变量的操作(read,load,assign等上述交互操作)不 可分割不可被打断,每个操作都要完整的执行完成才可以有其他操作进来。且默认对基本数据类 型的访问和读写都是原子性的(64位的long型和double型会有可能被拆分成两个32位进行读写 操作,但是这种概率极低,可以忽略不计。)
③有序性:为了提升效率,编译器会对代码进行乱序优化,而CPU会乱序执行,但是这样的操作 会导致很严重的问题。为了解决这一问题,使用了内存屏障来防止乱序的发生。 这样按照顺序执 行就是有序性。
进入主题
Volatile是什么?
Volatile是轻量级的synchronized锁,所谓轻量级,是因为synchronized使用时会引起线程的上下文切换,使得执行成本更高,效率更低,而Volatile不会有这些问题,效率更高。
Volatile特性:
1.可见性 :Visibility
①定义:当一个线程修改了共享变量的值以后,其他线程能够马上得知这个修改。
②先看一个例子:
package Test; public class VolatileTest {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("主线程等待线程A修改数据~~~");
while (!flag){}
System.out.println("主线程发现数据被线程A修改~~~~");
}
}).start();
Thread.sleep(300); new Thread(new Runnable() {
@Override
public void run() {
changeData();
}
}).start(); } public static void changeData(){
flag = true;
System.out.println("线程A修改完成数据~~~~");
} }
// 执行结果:
等待线程线程A修改数据~~~
线程A修改完成数据~~~~ 可以看出,当线程A修改完成数据后,另外一个线程应该要输出
主线程发现数据被线程A修改~~~~
,但是实际的运行情况是主线程一直处于等待状态。而如果把public static boolean flag = false;
修改为public static volatile boolean flag = false;
,也就是把变量用volatile
修饰,此时的执行结果:等待线程线程A修改数据~~~
线程A修改完成数据~~~~
线程A修改数据完成~~~~
很明显,线程A修改变量后,主线程也能感知到,使得数据具有可见性,这就是volatile的作用。
③volatile可见性底层实现原理:
对未加volatile修饰的变量修改时的底层汇编码:
对volatile修饰的变量修改时的底层汇编码:
由底层汇编可知,对volatile修饰的变量修改时,汇编指令前面会多一个lock
前缀,这个lock 前缀将会导致下面两件事发生:
(1)立即将修改过的数据回写到主内存中,刷新原来的数据。
在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
ps:摘自https://blog.csdn.net/yu280265067/article/details/50986947
(2)如果其他处理器缓存了这个被修改过的数据,那回写操作会使他们失效。
IA-32处理器和Intel64位处理器使用MESI(缓存一致性)维护内部缓存和其他处理器缓存的一致性,在多核处理器(多线程)中,处理器和线程使用嗅探技术检测各自缓存中的数据和总线上传递的数据是否一致,如果检测到有其他处理器或线程回写数据,且该数据是共享数据,那么就会强制使其他缓存了该数据的缓存中的数据失效。
2.有序性(禁止指令重排序):Odering
(1)指令重排序:为了优化和性能,编译器和处理器经常会对指令做重排序,且分为三种。
①编译器重排序:在不改变单线程程序语义的前提下,重新安排代码执行顺序。
②指令级并行重排序:处理器采用指令级并行技术将多条指令重叠执行,如果数据不存在 依赖,可以改变机器指令执行。
③内存系统重排序:处理器使用缓存和读/写缓冲区,使得加载和存储看上去是乱序执行。
重排序顺序示意图
先看一个例子:
package Test;
public class NoReoder {
private static int a = 0;
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
write();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
read();
}
}).start();
}
public static void write(){
a = 30;
flag = true;
System.out.println("方法一结束~~");
}
public static void read(){
if (flag ) {
a = a + 10;
System.out.println(a);
System.out.println("方法二结束~~");
}
}
}
当线程A启动调用了write方法,线程B启动调用read方法时能不能知道a被write方法修改了呢?答案是:不一定!!!
由于write方法中:
a = 30;
flag = true;
这两个操作的数据没有依赖性,所以可能会被重排序为:
flag = true;
a = 30;
这样就会使得read方法先读到flag = true ,而 a 还没修改完,从而使计算结果出错。
为了解决这种问题,在JMM中设计了内存屏障技术:
简单来说,Volatile的有序性就是靠内存屏障来实现,就是把一些操作限制在某些操作之前或者之后,比如将Store操作限制在Load之前,这样就能让其他线程得到的数据是最新的或者需要先写入数据再让其他线程加载数据。
说在最后:
相关参考,详见《Java并发编程的艺术》一书
本文仅是对个人学习中一些理解的记录,鉴于水平有限或多或少存在错漏或不严谨之处,欢迎各位大神批评指正。码字不易,欢迎转载转发但请标注出处。
希望病毒早点结束,再难的日子里也要坚持学习,新年快乐,最后愿工作在与病毒抗争最前线的医护人员平安打完这场仗,加油!!!!
Java并发关键字Volatile 详解的更多相关文章
- Java并发编程--Volatile详解
摘要 Volatile是Java提供的一种弱同步机制,当一个变量被声明成volatile类型后编译器不会将该变量的操作与其他内存操作进行重排序.在某些场景下使用volatile代替锁可以减少 ...
- java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock
原文:java并发编程 | 锁详解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock 锁 锁是用来控制多个线程访问共享资源的方式,java中可以使用synch ...
- Java 并发 关键字volatile
Java 并发 关键字volatile @author ixenos volatile只是保证了共享变量的可见性,不保证同步操作的原子性 同步块 和 volatile 关键字机制 synchroniz ...
- Java并发—— 关键字volatile解析
简述 关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制,当一个变量定义为volatile,它具有内存可见性以及禁止指令重排序两大特性,为了更好地了解volatile关键字,我们可以 ...
- java并发编程 | 线程详解
个人网站:https://chenmingyu.top/concurrent-thread/ 进程与线程 进程:操作系统在运行一个程序的时候就会为其创建一个进程(比如一个java程序),进程是资源分配 ...
- Java并发基础知识点详解
1.synchronized与Lock区别 父类有synchtonized,子类调用父类的同步方法,是没办法同步的,因为synchronized不是修饰符,不会被继承下来. synchronized ...
- Java并发--lock锁详解
在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方 ...
- java并发lock锁详解和使用
一.synchronized的缺陷 synchronized是java中的一个关键字,也就是说是Java语言内置的特性.那么为什么会出现Lock呢? 在上面一篇文章中,我们了解到如果一个代码块被syn ...
- Java并发--ReentrantLock原理详解
ReentrantLock是什么? ReentrantLock重入锁,递归无阻塞的同步机制,实现了Lock接口: 能够对共享资源重复加锁,即当前线程获取该锁,再次获取不会被阻塞: 支持公平锁和非公平锁 ...
随机推荐
- MySQL Workbench: mysqldump version mismatch
Windows10 64bit系统下,步骤就是: Edit --> preferences --> Administrator --> Path to mysqldump tool: ...
- GetDc函数与GetWindowDC函数的区别
GetDc函数:用于获得hWnd参数所指定窗口的客户区域的一个设备环境 GetWindowDC函数:返回hWnd参数所指定的窗口的设备环境. 获得的设备环境覆盖了整个窗口(包括非客户区),例如标题栏. ...
- VisualStudio 2019 新特性
很多小伙伴都好奇 VisualStudio 2019 有哪些功能,下面让我介绍一些好玩的特性 在安装完成之后会看到创新的欢迎界面,这个欢迎界面支持输入关键字搜项目,同时支持选择语言平台 很多小伙伴都说 ...
- ArcGIS-PictureMarkerSymbol-向地图添加图片标记
1.基于4.13 版本 <link rel="stylesheet" href="https://js.arcgis.com/4.13/esri/themes/li ...
- 学习Java第七周
重要知识点 1.“super”的用法 构造器和方法,都用关键字super指向超类,但是用的方法不一样.方法用这个关键字去执行被重载的超类中的方法 2.接口和抽象类的异同 相同: 1.接口和抽象类都有抽 ...
- Java程序员必备:异常的十个关键知识点
前言 总结了Java异常十个关键知识点,面试或者工作中都有用哦,加油. 一. 异常是什么 异常是指阻止当前方法或作用域继续执行的问题.比如你读取的文件不存在,数组越界,进行除法时,除数为0等都会导致异 ...
- 【一起学源码-微服务】Nexflix Eureka 源码四:EurekaServer启动之完成上下文构建及EurekaServer总结
前言 上篇文章已经介绍了 Eureka Server上下文创建相关的Eureka Client逻辑,这一部分还是比较复杂的.接下来就讲解下Eureka Server上下文初始化最后的部分,然后加上整个 ...
- 基于WPF&Prism&AvalonEdit的XAML轻量编辑器
1. 写在前面 一直从事WPF的相关开发工作,有时为了尝试或演示某些仅仅基于XAML的效果时,但又不想大动干戈打开VS去创建项目,所以一个轻便简单,集编辑与预览于一身的XAML编辑器就显得格外重要. ...
- C# 启动 a Python Web Server with Flask
概览 最近有个需求是通过c#代码来启动python 脚本.嘿~嘿!!! 突发奇想~~既然可以启动python脚本,那也能启动flask,于是开始着手操作. 先看一波gif图 通过打开控制台启动flas ...
- 02_css3.0 前端长度单位 px em rem vm vh vm pc pt in 你真的懂了吗?
1:废话不多说,直接看如下图表: 2:px就不过多介绍了,就是像素点的大小,加入您的屏幕分辨率为1920,则每一个相当于每一个有横着的1920个像素点: 3:em 为相对单位,一般以 body 内的 ...