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操作修改的值重新覆盖至工作内存后,到写回主内存这个流程前后加锁和释放锁,相对总线加锁而言,需要耗时量很小,对其他线程操作数据不存在太大的影响。

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值