Java 内存模型(JMM) 以及volatile关键字实现原理

网上JMM 一大堆 但是我觉得 大部分不结合实际代码去一点点了解,看了也只是一知半解,最后都会忘记!

要学习内存模型先来简单看下现代计算机多级并发缓存架构

多级并发缓存架构:

其中CPU 缓存在现在系统中为L1,L2,L3 缓存.

Java 内存模型(JMM) 以及volatile关键字实现原理

JVM 内存模型:准确的来说Java 线程内存模型 和CPU缓存模型类似,也是基于CPU缓存模型来建立的,Java 线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别.

Java 内存模型(JMM) 以及volatile关键字实现原理

了解了大概,我们结合代码去瞅瞅~ 先上代码:

@Slf4j
public class VolidateVisibility {
   private static boolean isFlag = false;
   public static void main(String[] args) throws InterruptedException {
       new Thread(() -> run()).start();
       Thread.sleep(1000);
       new Thread(() -> run2()).start();
   }
   private static void run() {
       log.info(\\\"start use prepare date...\\\");
       while (!isFlag) {
       }
       log.info(\\\"end prepare date...\\\");
   }
   private static void run2() {
       log.info(\\\"prepare date...\\\");
       isFlag = true;
       log.info(\\\"prepare date end ...\\\");
   }
}
23:13:29.223 [Thread-0] INFO com.ckj.bloghunter.domain.service.common.util.VolidateVisibility - start use prepare date...
23:13:30.225 [Thread-1] INFO com.ckj.bloghunter.domain.service.common.util.VolidateVisibility - prepare date...
23:13:30.225 [Thread-1] INFO com.ckj.bloghunter.domain.service.common.util.VolidateVisibility - prepare date end ...

出现这个结果对于了解过JMM的应该是毫无意外! 线程1 的 isFlag变量还是false! 并没有收到线程2 isFlag=true 的影响! 其实解决这个很简单再变量上加volatile关键字即可!,但是为什么volatile 可以实现不同线程之间变量的可见呢? 我们接下来结合流程图以及代码和汇编代码来解释.

如果线程Thread-1在自己的本地内存中修改共享变量的副本时如果不及时刷新到主存并通知Thread-2从主存中重新读取的话,那么Thread-2将看不到Thread-1所做的改变并仍然我行我素的操作自己内存中的共享变量副本。这也就是我们常说的Java内存模型(JMM)。

那么线程该如何和主存交互呢?JMM定义了以下8种操作以满足线程和主存之间的交互,JVM实现必须满足对所有变量进行下列操作时都是原子的、不可再分的(对于double和long类型的变量来说,load、store、read、write操作在某些平台上允许例外)

  • read(读取): 从主内存中读取数据

  • load(载入): 将主内存读取到的数据写入工作内存

  • use(使用):  从工作内存读取数据来计算

  • assign(赋值): 将计算好的数据重新赋值到工作内存中

  • store(存储): 将工作内存数据写入主内存

  • write(写入): 将store 过去的变量值赋值给主内存中的变量

  • lock(锁定): 将主内存变量加锁,标识为线程独占状态

  • unlock(解锁): 将主内存变量解锁,解锁后其他线程可以锁定该变量

如果需要和主存进行交互,那么就要顺序执行read、load指令,或者store、write指令,注意,这里的顺序并不意味着连续,也就是说对于共享变量a、b可能会发生如下操作read a -> read b -> load b -> load。

解决不同线程之间变量可见性(JMM缓存不一致)问题:

  • 总线加锁(性能太低) : cpu从主内存读取数据到高速缓存,会在总线对这个数据进行加锁,这样其他cpu没法读或写这个数据,直到这个cpu使用完数据释放锁之后其他cpu才可以读取该数据.

可以类比代码在线程2 read的时候 就对主内存这个变量加锁lock ,一直到最后write 结束 才释放这个主内存变量,可想而知是多么的影响性能, 所以结果也对应是涉及到同一个变量操作的线程 变成串行执行,对应的线程1 工作内存读到的肯定是最新的值initFlag=true,很老之前是这样处理的,现在已经废弃了.

Java 内存模型(JMM) 以及volatile关键字实现原理

  • MESI 缓存一致性协议: 多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效.

同样可以类比代码中线程2数据一到总线 ,就会触发线程1嗅探机制失效掉对应工作内存中的变量.

失效掉线程1的工作内存中变量,当线程1 再次使用initFlag 变量的时候就会再次load 主内存的数据,从而获取到最新的数据, 但是会有人会有这样的疑问:线程2的数据经过总线 store 主内存,这个时候已经触发总线上的嗅探导致线程1读的数据还是一开始的值,因为 线程2 的write还没有执行,所以这个时候就有了在store过程的加锁lock,避免这个问题如下图所示,只有主内存write 结束, 这个主内存变量才会被其余线程所访问,这个加锁的粒度是非常非常小的!这也是volatile关键字的底层大概实现原理.

既然聊到volatile 那我们就一起看看它的具体实现!

Volatile可见性底层实现原理:底层实现主要是通过汇编lock 前缀指令,它会锁定这块内存区域的缓存并回写到主内存,此操作被称为“缓存锁定”,MESI缓存一致性协议机制会阻止同时修改被两个以上处理器缓存的内存区域数据.一个处理器的缓存值通过总线回写到主内存会导致其他处理器相应的缓存失效.

对应线程2的store 操作触发lock,lock前缀指令相当于一个内存屏障,会强制将对缓存的修改操作写入主内存

并发编程三大特性:可见性,原子性,有序性, volatile保证可见性与有序性,但是不保证原子性,保证原则性需要借助synchronized这样的锁机制.

原创文章,作者:小技术君,如若转载,请注明出处:https://www.sudun.com/ask/33910.html

(0)
小技术君's avatar小技术君
上一篇 2024年4月14日 上午11:13
下一篇 2024年4月14日 上午11:15

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注