小熊回收站|分析:volatile内存屏障+实现原理(JMM和MESI)

推荐学习

  • 开心到飞起!Alibaba百万年薪必备—高性能架构路线已到手
  • 死磕「并发编程」100天 , 全靠阿里大牛的这份最全「高并发套餐」
初识volatileJava语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量 , 为了确保共享变量能被准确和一致地更新 , 线程应该确保通过排他锁单独获得这个变量 。 这个概念听起来有些抽象 , 我们先看下面一个示例:
【小熊回收站|分析:volatile内存屏障+实现原理(JMM和MESI)】package com.zwx.concurrent;public class VolatileDemo {public static boolean finishFlag = false;public static void main(String[] args) throws InterruptedException {new Thread(()->{int i = 0;while (!finishFlag){i++;}},"t1").start();Thread.sleep(1000);//确保t1先进入while循环后主线程才修改finishFlagfinishFlag = true;}}这里运行之后他t1线程中的while循环是停不下来的 , 因为我们是在主线程修改了finishFlag的值 , 而此值对t1线程不可见 , 如果我们把变量finishFlag加上volatile修饰:
public static volatile boolean finishFlag = false;这时候再去运行就会发现while循环很快就可以停下来了 。 从这个例子中我们可以知道volatile可以解决线程间变量可见性问题 。 可见性的意思是当一个线程修改一个共享变量时 , 另外一个线程能读到这个修改的值 。
volatile如何保证可见性利用工具hsdis , 打印出汇编指令 , 可以发现 , 加了volatile修饰之后打印出来的汇编指令多了下面一行:
小熊回收站|分析:volatile内存屏障+实现原理(JMM和MESI)lock是一种控制指令 , 在多处理器环境下 , lock 汇编指令可以基于总线锁或者缓存锁的机制来达到可见性的一个效果 。
可见性的本质硬件层面线程是CPU调度的最小单元 , 线程设计的目的最终仍然是更充分的利用计算机处理的效能 , 但是绝大部分的运算任务不能只依靠处理器“计算”就能完成 , 处理器还需要与内存交互 , 比如读取运算数据、存储运算结果 , 这个 I/O 操作是很难消除的 。 而由于计算机的存储设备与处理器的运算速度差距非常大 , 所以现代计算机系统都会增加一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存和处理器之间的缓冲:将运算需要使用的数据复制到缓存中 , 让运算能快速进行 , 当运算结束后再从缓存同步到内存之中 。 查看我们个人电脑的配置可以看到 , CPU有L1,L2,L3三级缓存,大致粗略的结构如下图所示:
小熊回收站|分析:volatile内存屏障+实现原理(JMM和MESI)从上图可以知道 , L1和L2缓存为各个CPU独有 , 而有了高速缓存的存在以后 , 每个 CPU 的处理过程是 , 先将计算需要用到的数据缓存在 CPU 高速缓存中 , 在 CPU进行计算时 , 直接从高速缓存中读取数据并且在计算完成之后写入到缓存中 。 在整个运算过程完成后 , 再把缓存中的数据同步到主内存 。 由于在多 CPU 中 , 每个线程可能会运行在不同的 CPU 内 , 并且每个线程拥有自己的高速缓存 。 同一份数据可能会被缓存到多个 CPU 中 , 如果在不同 CPU 中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题 , 那么怎么解决缓存一致性问题呢?CPU层面提供了两种解决方法:总线锁和缓存锁
总线锁总线锁 , 简单来说就是 , 在多CPU下 , 当其中一个处理器要对共享内存进行操作的时候 , 在总线上发出一个 LOCK#信号 , 这个信号使得其他处理器无法通过总线来访问到共享内存中的数据 , 总线锁定把 CPU 和内存之间的通信锁住了(CPU和内存之间通过总线进行通讯) , 这使得锁定期间 , 其他处理器不能操作其他内存地址的数据 。 然而这种做法的代价显然太大 , 那么如何优化呢?优化的办法就是降低锁的粒度 , 所以CPU就引入了缓存锁 。


推荐阅读