01-JMM&volatile

本文详细介绍了Java内存模型(JMM)、volatile关键字的作用以及其与缓存一致性协议MESI的关系。volatile确保了变量的及时可见性和禁止指令重排,但不保证原子性。MESI协议保证了多核CPU缓存的一致性。文章还探讨了JVM、JMM和CPU执行流程,并分享了一个有趣的线程同步现象。
摘要由CSDN通过智能技术生成


一、什么是JMM

1、java内存模型。是一种抽象的概念,描述的是一组规范,定义了程序中各个变量(包括实例字段,静态字段和数组元素)的访问方式。
2、jvm运行程序的实体是线程,而每个线程创建时jvm都会为其创建一个工作内存(栈空间),用于存储线程私有数据,而java内存模型规定所有变量都存储在主内存,主内存是共享区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行。首先将变量从主内存拷贝到自己的工作内存中,然后对变量进行操作,操作完成后再将变量写回主内存,其他线程通过嗅探机制,让自己工作内存的数据失效,然后从主内存中重新load最新的值,从而完成线程间的数据交互。不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本,工作内存是私有区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

二、volatile

volatile是java轻量级的锁机制,不能保证线程安全。
1、volatile保证了可见性,及时可见性。如果不使用volatile,当前线程也可以看见另一个线程回写到主内存中的值,只是不是及时的看到。加了volatile,在底层字节码层面,会给变量加一个ACC_VOLATILE的标记。
2、为什么volatile不能保证原子性呢
int count++
(1)发生了写覆盖。还是某个线程的操作丢了。我倾向于写覆盖。
3、有序性,禁止指令重排。为什么会指令重排呢,因为指令重排后效率可能更高,所以会指令重排,指令重排不是一定出现的。但是指令重排不能影响程序的结果,否则就没有意义。
为什么volatile可以禁止指令重排呢,因为变量加了volatile之后,底层会在变量读和写的时候加内存屏障。内存屏障会告诉编译器其他指令不要和加了volatile的这条指令重排,从而保证了有序性。

三、缓存一致性协议MESI

1、volatile修饰的变量,在写操作的时候底层会加一个lock前缀的指令,lock指令会触发硬件缓存锁定机制,实现这种机制有两种方式,总线锁和缓存一致性协议(MESI是主流的缓存一致性协议的实现)。硬件缓存锁定机制是为了避免多个线程同时修改同一个变量。
2、早期的cpu就是给总线加锁来实现的,cpu和内存之间的访问是通过总线桥来进行的,通过给总线加锁,可以避免多个线程同时修改同一个变量,但是这种总线锁的方式有一个缺点,就是一个线程给总线加了锁,加锁期间其他线程连读取操作都不能进行了,效率太低了。
3、所以引出了缓存一致性协议,MESI是主流的缓存一致性协议的实现。M表示修改,E表示独占,S表示共享,I表示失效。线程1和线程2同时修改变量,最开始线程1将变量的值从主存中拷贝到L3缓存,此时只有一个线程持有变量的副本,线程1中变量的状态为E独占状态,当线程2也从主存中拷贝变量值到L3缓存中时,此时有多个线程持有变量的副本,那么线程2将变量的状态修改为S共享状态,然后发送一个信号给bus总线,线程1通过总线嗅探机制,知道有其他线程也持有了变量的副本,此时将线程1中变量的状态修改为S共享状态。两个线程都将数据读到了自己的L1缓存中,那么到底哪个线程能够修改成功呢。如果哪个线程给自己的缓存加锁成功,则哪个线程持有变量的副本的状态就修改为E独占状态,并且发送信号给bus总线,通知其他线程将自己副本的状态修改为I失效状态。每个线程都有自己独占的缓存行,如果两个线程都同在同一时刻给自己的缓存行加锁成功了呢,这时候就需要总线裁决了,总线裁决是一瞬间的事情,效率很高。
4、如果变量很大,一个缓存行64byte装不下,需要两个缓存行,则这时候直接使用总线锁的方式,弃用缓存一致性协议,谁加锁成功,则可以修改变量。
为什么这种情况不能使用缓存一致性协议呢,因为一个缓存行可以保证原子的,锁两个缓存行就不能保证原子的。
5、volatile i++不能保证原子性的原因,i++是两步操作,当线程1将i读取到自己的工作内存,且已经在寄存器做了自增操作,但还未将计算好的值写回自己的工作内存,最终写回主内存。此时cpu调度切换到线程2,线程2将i读取到自己的工作内存中,且线程2给自己的缓存行加锁成功,通过MESI协议可知,此时有多个线程持有副本,线程2发送消息通知其他线程失效,但是线程1已经将数据读取到了寄存器在做自增操作了,此时虽然失效了线程1的变量,但是自增操作的结果已经计算好了,这时候线程1还是会把数据写回自己的工作内存,最终写回主内存,从而发生了写覆盖,线程不安全,这就是volatile不能保证原子性的原因。

四、JVM-JMM-CPU底层执行流程

java是基于栈指令架构集的,jvm站在硬件的角度来讲,堆和栈都是运行在内存中的。字节码执行引擎负责执行字节码,当执行引擎需要把一个变量压入栈中的时候,栈是在内存中,执行引擎操作的是cpu,将一个变量通过cpu压入栈中。cpu只认识0101二进制的机器指令,那么就需要解释执行器/JIT优化将字节码翻译为汇编指令,硬件原语,但是cpu还是不认识啊,然后硬件进一步将汇编指令(硬件原语)翻译为二进制,最终翻译为cpu能够识别的语言。翻译出来的二进制指令会等待cpu调度才能执行。字节码只有JVM字节码执行引擎可以识别,cpu与底层硬件无法识别执行,字节码需要被编译器编译为计算机硬件汇编指令,然后硬件解释翻译汇编指令,最终翻译为计算机可执行的二进制机器指令。

五、有趣的现象

1、是不是不加volatile,一个线程就不能看见另一个线程对变量的更新。
背景:线程1死循环,线程2修改变量,线程1不能看见线程2的更改。
(1)int,在线程1里面对另一个int变量进行自增操作,线程1不能看见线程2的更改。
(2)Integer,在线程1里面对另一个Integer变量进行自增操作,线程1可以看见线程2的更改。
(3)int,在线程1里面对另一个volatile修饰的int变量进行自增操作,线程1可以看见线程2的更改。
(4)在线程1里面打印system.out.println,线程1可以看见线程2的更改。
(5)关掉JIT,(JIT及时编译,热点代码优化,将热点代码编译为机器语言,这样执行是最快的,idea启动命令默认加了JIT),线程1可以看见线程2的更改。
可能的解释:
(1)一个线程的缓存行,会缓存多个变量,线程从主内存中load最新的数据,是按一个缓存行来更新的。所以给线程1的另一个变量加了volatile,线程1也可以看见线程2的更新,因为可能两个变量在同一个缓存行。
(2)而加了system.out.println,while空循环的优先级是很高的,一旦得到cpu的执行权,基本不会释放。在其中加了其他的执行语句,就可能使线程释放执行权。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值