java学习总结——volatile关键字(一)

一、初识

在我们日常的计算机处理数据时,都是cpu、主内存、缓存、高速缓存。
在这里插入图片描述
不了解RAM的可以参考文章:RAM和ROM的区别

为什么会有高速缓存这个说法?

现在的cpu越来越贵,贵的不是材料,而是每一代的处理运算效率,这个效率的快慢,就在于cpu寄存器cpu高速缓存的处理效率上,通俗而言,cpu的处理速率是远远高于主内存的处理速率的,为了防止cpu处理速率受限于主内存,所以在cpu和主内存之间加入了高速缓存。

最近有朋友问我知不知道volatile这个关键字,以及这个关键字的作用和底层实现的流程是什么,完全懵逼,所以查找各种资料,大致总结了下volatile关键字的知识。

接下来我们一起学习volatile关键字吧。

二、了解volatile

大家都知道volatile关键字的作用,简单而言,就是为了保证多线程变量副本的可见性
可能这个概念太抽象了,我们接下来一起看几个demo吧
demo1:无volatile关键字修饰变量

public class VolatilePreDemo {
	public static boolean isChanged = false;
	public static void main(String[] args) throws InterruptedException {
		//开启线程一  
		new Thread(()->{
			System.out.println("等待数据-------");
			//死循环  等待数据变更(当全局变量变更,则死循环结束)
			while(!isChanged){
			}
			System.out.println("数据变更了------");
		}).start();
		//主线程等待
		Thread.sleep(3000);
		//线程二
		new Thread(()->{
			changeData();
		}).start();
		
	}
	public static void changeData(){
		System.out.println("数据变更前");
		isChanged = true;
		System.out.println("数据变更了");
	}
}

代码流程的解析:
开启两个线程,线程一死循环等待全局变量的修改,如果修改成功,则打印数据变更了------
线程二在主线程等待3000毫秒后开启,主要用于修改全局变量值。

运行后的结果:
在这里插入图片描述
我们发现此时有两个现象:
1、线程二变更了全局变量值,但线程一中的数据并未获取到变更后的操作。(类似数据两者被隔离处理)
2、程序执行后,就一直在运行,没有正常走完。(死循环)

所以可以了解到:
线程二修改的"共享变量"并不会影响到其他内存。
主内存中的"共享变量"加载至各项线程中是副本,并非数据主体本身。

在这里插入图片描述


如何才能实现我线程二修改全局变量后,线程一也能收到影响呢?

所以我们接下来一起看volatile

demo2:有volatile关键字修饰变量

public class VolatileDemo {
	public static volatile boolean isChanged = false;
	public static void main(String[] args) throws InterruptedException {
		//开启线程一  
		new Thread(()->{
			System.out.println("等待数据-------");
			//死循环  等待数据变更(当全局变量变更,则死循环结束)
			while(!isChanged){
			}
			System.out.println("数据变更了------");
		}).start();
		//主线程等待
		Thread.sleep(3000);
		//线程二
		new Thread(()->{
			changeData();
		}).start();
		
	}
	public static void changeData(){
		System.out.println("数据变更前");
		isChanged = true;
		System.out.println("数据变更了");
	}
}

运行后的结果:
在这里插入图片描述

三、JMM(java memory mode)数据原子操作

标识 名称 描述
read 读取 从主内存中读取数据
load 载入 将主内存读取到的数据写入工作内存
use 使用 从工作内存读取数据来计算
assign 赋值 将计算好的值重新赋值到工作内存中
store 存储 将工作内存数据写入主内存
write 写入 将store过去的变量值赋值给主内存中的变量
lock 锁定 将主内存变量加锁,标识为线程独占状态
unlock 解锁 将主内存变量解锁,解锁后其他线程可以锁定该变量

我们接下来再来看上面的第二个demo。两个线程数据处理分别的流程是什么?

3.1、开始处理数据前

在这里插入图片描述

3.2、线程一处理流程

在这里插入图片描述

1、程序运行,会将代码加载至内存中。
2、线程一执行,会将变量从主内存(RAM)中读取(read);并载入(load)至工作内存中。
3、cpu会读取工作区中的变量,进行计算,由于isChanged为false,所以此时为死循环,代码会停顿在循环处,不会打印其他信息。

3.3、线程二的处理流程

在这里插入图片描述

1、程序运行,会将代码加载至内存中。
2、线程二执行,会将变量从主内存(RAM)中读取(read);
并载入(load)至工作内存中。
3、线程二会使用(use)工作内存中的变量,载入并计算。
4、计算好了之后,将计算好的值重新赋值(assign)到工作内存中。
5、工作内存中的值重新被赋值后,会将值继续存储(store)至主内存中。(此时只是存储在主内存中,并未修改主内存中的数据)
6、将store过去的变量值赋值给主内存中的变量(write)。

以上分析是截至demo1的流程,类比线程一和线程二的流程处理,我们发现,线程二变更了主内存中的“共享变量”,但并未及时的通知线程一(线程一和线程二是两个分开的操作,执行的“共享变量”是副本)。

demo2中全局变量isChangedvolatile修饰,此时在线程二处理完成后,又有了什么操作流程呢?

了解处理流程之前,我们先查看发展历史。参考以下资料:
总线锁、缓存锁、MESI
缓存一致性协议MESI和MOESI
并发编程-JMM基础

在《并发编程-JMM基础》中有说到在多 cpu 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK# 信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,总线锁定的开销比较大,这种机制显然是不合适的。

一大段白话,可能相对难懂,说简单点就是

CPU从主内存中取得数据并放入高速缓存中,会在总线对这个数据加锁,让其他cpu没法去读或写这个数据,直到这个cpu使用完后释放锁,其他cpu才能继续操作该数据。

在使用总线加锁机制操作数据时,其他线程需要操作数据,必须等待拿到数据的那个线程释放锁,性能太低(多核处理数据为并行,加锁类似消息队列挨个进行),所以后续出现了一个新的方式——MESI缓存一致性协议。

MESI缓存一致性协议原理:《总线锁、缓存锁、MESI
多个cpu从主内存中读取同一个数据到各自的高速缓存区中,当其中某个cpu对这个数据进行了修改操作,该数据会及时同步到主内存中,其他cpu通过总线嗅探机制感知到数据的变化从而将自己高速缓存分区(工作副本) 中的数据失效(只要数据变更经过了总线,则会触发 嗅探/监听 ),然后重新加载。

额外补充总线
数据传输的共有通道。

关于volatile关键字修饰的变量,像demo2一样,线程二变更“共享变量”信息后,线程一能够嗅探主缓存中的对应变量信息变更,及时的失效自己cpu中的变量信息,重新从主缓存中加载最新的信息值。
在这里插入图片描述

如上图所示:
线程二变更了变量,经过store(将工作内存数据写入主内存)操作,由于经过了总线,触发了其他线程对该变量的监听,使得其他线程会失效工作区内的共享变量副本信息,重新从主内存中进行获取新的变量信息。

上面我们了解了MESI缓存一致性协议和==各个线程处理数据时,volatile的处理方式,那么volatile处理“共享资源”的原理是什么呢?

四、volatile实现可见性的原理

此处查看资料:
volatile底层实现原理和其应用
怎么查看Java代码对应的汇编
volatile的底层实现原理

在《volatile的底层实现原理》中,这篇博客讲解了如何查看java代码运行时的汇编语句,其中的demo明确的展示了被volatile关键字修饰的变量在汇编中是如何操作的。

总结上述资料中的各种说法,volatile缓存可见性实现原理为:

底层实现主要是通过汇编lock前缀指令,
他会锁定这块区域的缓存(缓存行锁定)并写回到主内存

IA-32架构软件开发者手册对lock指令的解释:

1、会将当前处理器缓存行的数据立刻写回到系统内存(store、write) —— 不管后面是否还有其他业务代码,都会先同步回主内存
2、这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)。

如果上面的解释还是不能理解,可以参考下图的标注:
在这里插入图片描述

关于java代码汇编查看IA-32架构软件开发者手册,可以查看链接:
https://github.com/765199214/java-tools

五、总线加锁和MESI缓存一致性协议的区别

名称 总线加锁 MESI
加锁区域: 主内存开始加锁 store操作之前开始加锁
释放锁区域: 主内存释放锁 同步后(write操作完成)释放锁

MEISI缓存一致性协议为什么需要加lock的操作?

场景一:多个线程执行相同的操作(如:变更全局变量),可能同时会去对主内存进行数据变更操作,此时会出现并发问题。

场景二:store操作到总线时,其他线程操作感知到总线数据的变动,对其所在的线程中的工作内存中的信息进行失效操作。
但此时变量数据变更的操作并未成功执行完主内存中的数据变更(write)。其他线程重新获取的值依旧还是未变更的原始值,并且变更的线程不再触发总线操作了。

同样是都需要进行加锁操作,为什么MESI协议的加锁比总线加锁更好?

1、总线的加锁和释放锁是在主内存中进行的,加锁操作后,需要等待一个线程执行完全的 read–load–use–assign–store–write 流程后,才能释放锁,占据了数据操作的整个流程,极大的占用了系统资源和影响了处理数据的效率问题。
2、MESI协议加锁只是在assign操作,将use操作修改的值重新覆盖至工作内存后,到写回主内存这个流程前后加锁和释放锁,相对总线加锁而言,需要耗时量很小,对其他线程操作数据不存在太大的影响。

猜你喜欢

转载自blog.csdn.net/qq_38322527/article/details/103235148
今日推荐