volatile关键字的实现原理

volatile主要是通过Lock汇编指令(MESI协议(IA-32和Intel64处理器下))前缀来实现的。

以下是学习volatile关键字时学习的相关知识。

提升计算机性能的部分措施

1)高速缓存:由于磁盘的IO速度和CPU的处理速度不在一个数量级,所以引入了高速缓存。高速缓存在CPU内部,将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

2)编译器的指令优化(指令重排):为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。

3)多线程:操作系统增加了进程、线程。通过 CPU 的时间片切换最大化的提升 CPU 的使用率。

4)多核

引入以上机制后存在的部分问题与解决方法

2.1 缓存一致性问题

同一份数据可能会被缓存到多个CPU中,如果在不同CPU中运行的不同线程看到同一份内存的缓存值不一样。主要的两种解决办法(硬件层面):

1.总线锁

简单来说就是,在多CPU下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。

2.缓存一致性协议:为了控制锁的保护粒度,只要保证对于被多个CPU缓存的同一份数据是一致的就行。所以引入了缓存锁,它通过缓存一致性协议来实现。这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及 Dragon Protocol等。

CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

2.2 多核时指令重排后结果与预期不一致问题

3 Java内存模型中对主内存与工作内存间数据操作的八种基本操作(均为原子操作)

Java内存模型中定义了以下8种操作来完成主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节。虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许有例外)。

lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。

assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

4 Java内存模型规定了在执行上述8种基本操作时必须满足如下规则

        1.不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。如果要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和write操作。注意,Java内存模型只要求上述两个操作必须按顺序执行,而没有保证是连续执行。也就是说,read与load之间、store与write之间是可插入其他指令的。

        2.不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

        3.不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。

        4.一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。

        5.一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

        6.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

        7.如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

        8.对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。

5 volatile关键字的作用和原理

volatile关键字有两个作用,一是保证可见性,二是禁止指令重排。

实现原理:volatile主要是通过Lock汇编指令(MESI协议(IA-32和Intel64处理器下)和)前缀来实现的。

查看用volatile修饰的对象的汇编代码

 可以发现,赋值后多执行了一个“lock addl $0x0,(%esp)”操作。

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

1)会将当前处理器缓存行的数据立即写回到系统内存。

2)这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI协议)

3)提供内存屏障功能, 重排序时不能把后面的指令重排序到内存屏障之前的位置

​​​​​​​注:java内存模型规定所有变量都必须存储在主内存中,每个线程有自己的工作内存,存储的都是主内存的副本,线程间变量值的传递需要通过主内存来完成,java内存模型保证了原子性,有序性,可见性。

6 volatile不具有原子性

原因:Java中运算不是原子操作。

例如多个线程执行num++操作, 原本线程1在自己的工作空间中将num改为1,写回主内存,主内存由于内存可见性,通知线程2 3,num=1;线程2通过变量的副本拷贝,将num拷贝并++,num=2;再次写入主内存通知线程3,num=2,线程3通过变量的副本拷贝,将num拷贝并++,num=3; 然而多线程竞争调度的原因,1号线程刚刚要写1的时候被挂起,2号线程将1写入主内存,此时应该通知其他线程,主内存的值更改为1,由于线程操作极快,还没有通知到其他线程,刚才被挂起的线程1 将num=1 又再次写入了主内存,主内存的值被覆盖,出现了丢失写值。

引用:volatile关键字详解

深入解析volatile关键字

从汇编看volatile与MESI的关系

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值