Fork me on GitHub

volatile实现原理

Volatile含义

  • 在Java多线程并发编程中,volatile关键词扮演着重要角色,它是轻量级的synchronized,在多处理器开发中保证了共享变量的“可见性”。“可见性”的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。与synchronized不同,volatile变量不会引起线程上下文的切换和调度,在适合的场景下拥有更低的执行成本和更高的效率

CPU缓存

  • CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多,举个例子:
    • 一次主内存的访问通常在几十到几百个时钟周期
    • 一次L1高速缓存的读写只需要1~2个时钟周期
    • 一次L2高速缓存的读写也只需要数十个时钟周期
  • 这种访问速度的显著差异,导致CPU可能会花费很长时间等待数据到来或把数据写入内存

  • 基于此,现在CPU大多数情况下读写都不会直接访问内存(CPU都没有连接到内存的管脚),取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小得多但是交换速度却比内存快得多。而缓存中的数据是内存中的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度

  • 按照读取顺序与CPU结合的紧密程度,CPU缓存可分为:

    • 一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存。
    • 二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半。
    • 三级缓存:简称L3 Cache,部分高端CPU才有
  • 每一级缓存中所存储的数据全部都是下一级缓存中的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也相对递增

  • 当CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。一般来说每级缓存的命中率大概都有80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取

CPU缓存带来的问题

  • 用一张图表示一下 CPU –> CPU缓存 –> 主内存 数据读取之间的关系:
    cache
  • 当系统运行时,CPU执行计算的过程如下:

    • 程序以及数据被加载到主内存
    • 指令和数据被加载到CPU缓存
    • CPU执行指令,把结果写到高速缓存
    • 高速缓存中的数据写回主内存
  • 如果服务器是单核CPU,那么这些步骤不会有任何的问题,但是如果服务器是多核CPU,那么问题来了,以Intel Core i7处理器的高速缓存概念模型为例(图片来自《深入理解计算机系
    cache

  • 试想下面一种情况:

    • 核0读取了一个字节,根据局部性原理,它相邻的字节同样被被读入核0的缓存
    • 核3做了上面同样的工作,这样核0与核3的缓存拥有同样的数据
    • 核0修改了那个字节,被修改后,那个字节被写回核0的缓存,但是该信息并没有写回主存
    • 核3访问该字节,由于核0并未将数据写回主存,数据不同步
  • 为了解决这一问题,CPU制造商规定了一个缓存一致性协议

缓存一致性协议

  • 每个CPU都有一级缓存,但是,我们却无法保证每个CPU的一级缓存数据都是一样的。 所以同一个程序,CPU进行切换的时候,切换前和切换后的数据可能会有不一致的情况。那么这个就是一个很大的问题了。 如何保证各个CPU缓存中的数据是一致的。就是CPU的缓存一致性问题

总线锁

  • 一种处理一致性问题的办法是使用Bus Locking(总线锁)。当一个CPU对其缓存中的数据进行操作的时候,往总线中发送一个Lock信号。 这个时候,所有CPU收到这个信号之后就不操作自己缓存中的对应数据了,当操作结束,释放锁以后,所有的CPU就去内存中获取最新数据更新。

  • 但是用锁的方式总是避不开性能问题。总线锁总是会导致CPU的性能下降。所以出现另外一种维护CPU缓存一致性的方式,MESI

MESI

  • MESI是保持一致性的协议。它的方法是在CPU缓存中保存一个标记位,这个标记位有四种状态:

    • M: Modify,修改缓存,当前CPU的缓存已经被修改了,即与内存中数据已经不一致了;
    • E: Exclusive,独占缓存,当前CPU的缓存和内存中数据保持一致,而且其他处理器并没有可使用的缓存数据;
    • S: Share,共享缓存,和内存保持一致的一份拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段;
    • I: Invalid,失效缓存,这个说明CPU中的缓存已经不能使用了
  • CPU的读取遵循下面几点:

    • 如果缓存状态是I,那么就从内存中读取,否则就从缓存中直接读取。
    • 如果缓存处于M或E的CPU读取到其他CPU有读操作,就把自己的缓存写入到内存中,并将自己的状态设置为S。
    • 只有缓存状态是M或E的时候,CPU才可以修改缓存中的数据,修改后,缓存状态变为M
  • 这样,每个CPU都遵循上面的方式则CPU的效率就提高上来了

volatile两大作用

  • 保证内存可见性
  • 防止指令重排