之前说了volatile可以解决线程之间可见性问题 它到底是怎么解决的呢 它底层就利用到了 cpu系统指令的lock(前缀)触发缓存一致性协议 这就涉及到jvm跟硬件交互
首先当我们执行java代码 会加载到元空间 然后在堆中创建这个字节码的Class的对象 然后会创建线程栈执行方法 执行方法都是通过jvm模型的字节码执行引擎 当我们使用javap这个命令可以看到java代码
编译后的样子 这个样子只能jvm认识而底层cpu是不认识的 这时字节码执行引擎会把我们的字节码进行翻译 当然这个翻译也是c++翻译 因为jvm是c++写的嘛 它会把我们的字节码翻译成硬件原语(汇编语言)
然后由汇编语言把字节码翻译成二进制 然后给cpu执行 这样是从jvm到了硬件cpu 然后开始说可见性问题 可见性问题之前也说了 就是java的一个抽象模型没有具体的实现 只是一个java的规范因为jvm有不同版本
比如阿里有自己jvm 所以直接说的JMM就是java的模型只是一个规范没有实现 所以实现jvm的都要遵循这个规范 jmm规范之前也是说了就是 有一个主内存存储的东西跟jvm的原空间差不多 然后每个线程都会有自己的
工作内存 工作内存可以说相当于jvm中的线程栈当方法执行时要什么对象就从堆中获取什么对象 工作内存每个线程都有 线程只能访问本线程的工作内存 而工作内存的所有变量全是从主内存中获取 当一个线程更改了
主内存中变量的值 另一个也有这个变量的线程无法看到(无法及时看到 这个原因无法解释)无法看到就会发生变量赋值的值错误 这就是可见行问题 解决这个问题是可以加volatile这个修饰符 下面开始说这个volatile怎么做到的
上面说的jvm字节码执行引擎会把java代码翻译成硬件原语 当一个变量被volatile修饰了 在翻译的时候 会给这个变量加上一个lock为前缀的名字 当有了这个名字就会执行lock(前缀)指令 这个指令早期的cpu为总线锁 总线锁就是在
cpu到内存的那根总线上加上锁 假设多核cpu当一个cpu拿取了这个被volatile修饰的变量时 就会获取总线锁的钥匙 当另一个cpu想要获取时因为没有钥匙只能等着第一个cpu执行完把钥匙释放 其他的cpu才能继续执行 这就是总线锁
这是早期的cpu这样处理线程直接可见性问题 现在的处理方式为MESI缓存一致性协议 MESI 每个字母代表一个单词并代表一个意思 M为已修改 E为独占 S为共享 i为失效 当一个cpu获取了一个被volatile修饰的变量时加载到缓存
会给这个变量标记上E 当第二个cpu也获取了这个值就会把E修改为S cpu会有一个监听来监听被volatile修饰的变量当被读取就会被监听到 当第一个cpu要修改这个变量时 不能直接修改要在缓存行上加锁 如果加锁成功了才能修改其他的
cpu也是同理 只能给一个缓存行加锁 如果要加两个及以上加不了会升级为总线锁 如果同时有两个cpu都在缓存行上加锁了会在总线上触发总线裁决 让总线决定听从那个cpu的(具体那个cpu无法解释) 如果当一个cpu加锁成功另一个cpu没有加锁(只是读)
那个加锁了的cpu把值修改之后标记为M会去通知另一个也有这个变量的cpu 然后它就会把这个变量标记为i然后重新获取 这就是MESI缓存一致性协议 解决可见性问题
Store Bufferes(下面说的是它的作用跟风险)
用MESI会有大量的通知cpu让其把变量给失效掉 这就会有一个问题 那个发送这个信号的cpu无法及时知道所有cpu已经失效的消息这样就无法执行其他的逻辑 所以这时就引出了Store Bufferes 当一个变量被修改了不会及时更新数据 而是先把这个数据写到
Store Bufferes中然后等所有cpu都通知它已经把变量给失效了 再更新(就是重写回内存中 这个操作什么时候完成也无法保证) 失效的变量也会存到一个缓存中跟这个差不多
处理器会尝试从存储缓存(Store Bufferes)中读取值 但是它还没进行提交 这个解决方法成为Store Forwarding 它使得加载的时候 如果存储缓存中存在 则返回