【你好面试官】008 Java内存模型指volatile底层原理详解、多处理器原子操作实现原理

微信公众号:你好面试官
这里没有碎片化的知识,只有完整的知识体系。
专注于系统全面的知识点讲解,面试题目解析;
如果你觉得文章对你有帮助,欢迎关注、分享、赞赏

前言

二蛋几天没有收到面试通知,以为自己已经凉凉,没想到此时再次接到了面试邀请,于是在一个风和日丽的下午,二蛋如约坐在了面试官对面,开始了今天的面试。

 

面试官:小伙子,咱也不用继续介绍了,你也来了几次了,这次咱就开门见山吧。

 

正文

今天开始Java内存模型、并发编程相关的内容讲解。这一部分知识的重要性不必再过多强调,这是从入门级别到进阶过程中必须掌握的东西,几乎所有的Java中高级岗位都会要求Java并发、内存模型、虚拟机相关的技能,虚拟机相关的内容会在下一个系列继续讲解。

详细介绍下volatile?

  • 概念
    volatile是一个Java关键字,主要用于并发编程模块中,它是一个轻量级的synchronized,主要作用是在并发编程中保证共享变量的可见性。

    可见性是指当一个线程修改贡献变量时,其他线程能够感知到变量的变化。
    由于使用volatile不会引起线程的上下文切换和调度,所以开销较小,与synchronized相比使用成本更低,更加轻量。

  • volatile的底层原理
    用volatile修饰的变量进行写操作时,会多出一些汇编代码,是以 lock为前缀的汇编指令,这个指令会有以下两种效果:

    1. 将当前处理器缓存行的数据写回到系统内存。
    2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

每个线程使用一个变量都会从主内存中读取变量,然后缓存到自己的本地内存中,此时如果其他线程修改了当前变量的值,不会立即写回到主内存,即使写回到主内存,已经缓存了变量的线程不会感知到变量变化,所以也不会立即更新,就导致其他线程中的变量值不是最新的。如上图,ABC都缓存了X的值为10,而A线程将X值改为20,且不会立即写回到主内存,即使写回到主内存,BC线程下次使用X变量时,依然从自己的缓存中读取,所以对BC线程来说,X依然为10.

由于上边(1)的操作,只要变量值发生了变化,就会立即将新值写回到主内存,从而保证主内存中的变量值是最新的,同时,由于(2)的操作,其他线程缓存的此变量值无效,当其他线程下次需要用到此变量值的时候,就必须重新从主内存中读取,所以保证了变量的值是最新值。

 

使用volatile修饰的变量,由于多了上边两个步骤,就会保证任何一个线程修改了变量的值,其他线程都能立即感知到。

  • 如何让其他线程缓存的地址失效?
    在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线.上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

这一块知识需要与操作系统的相关知识联系起来,搞清楚内存与缓存之间的关系以及缓存一致性协议等等。

多处理器如何实现原子操作?

原子的意思是不能再进行分割的粒子,我们知道在化学中,原子是最小粒子。
原子操作在这里是指不可中断的一个或者一系列操作。

不同的处理器的实现方式可能不同,这里大概讲一下实现的原理:
首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取-一个字节时,其他处理器不能访问这个字节的内存地址。但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和跨页表的访问。但是,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

  1. 使用总线锁保证原子性
    第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。
    原因可能是多个处理器同时从各自的缓存中读取变量i,分别进行加1操作,然后分别写入 系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享 变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
    处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该 处理器可以独占共享内存。

  2. 使用缓存锁保证原子性
    在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下 使用缓存锁定代替总线锁定来进行优化。
    频繁使用的内存会缓存在处理器的L1、L2和L3高速缓存里,那么原子操作就可以直接在 处理器内部缓存中进行,并不需要声明总线锁。所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存 行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声 言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子 性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处 理器回写已被锁定的缓存行的数据时,会使缓存行无效。

有两种情况下处理器不会使用缓存锁定:

  1. 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定。
  2. 有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的 内存区域在处理器的缓存行中也会调用总线锁定。

以上两个机制,可以通过处理器提供的Lock前缀的指令来实现。

面试官:嗯,今天的面试就到这里吧,回去好好准备下一次面试吧。



这里没有碎片化的知识,只有完整的知识体系。
专注于系统全面的知识点讲解,面试题目解析;
如果你觉得文章对你有帮助,欢迎 关注、分享、赞赏

 

猜你喜欢

转载自www.cnblogs.com/hello-interviewer/p/12347188.html