JAVA内存模型,为啥线程要有自己的本地内存,CPU高速缓存

volatile作用总结

 1. 强制线程从公共内存中取得变量的值,而不是从线程的私有的本地内存(如CPU高速缓存)中,volatile修饰的变量不具有原子性(修改一个变量的值不能同步)。

 2. 保证volatile修饰的变量在被一个线程修改后,会被强制立即刷新到主存(可见性),其他线程如果有该变量的缓存行,会被设置为无效。

 3. 禁止指令重排(有序性)

   a.happen-before(作用是: 定义一些禁止编译优化的场景,保证并发编程的正确性)

   b.编译器在生成字节码时, 会在指令序列中插入内存屏障,会多出一个 lock 前缀指令

 内存屏障是一组处理器指令,解决禁止指令重排序内存可见性的问题,保证指令重排序后与之前的输出结果一 样,使性能得到优化。处理器在进行重排序时是会考虑指令之间 的数据依赖性。

  

 指令重排:Java 语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致。这个过程通过叫做指令的重排序。

 指令重排序存在的意义在于:JVM能够根据处理器的特性CPU多级缓存系统多核处理器等)适当的新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能,提高效率

使用 volatile 的场景:

  • 双重校验锁 DCL(double checked locking)
  • ConcurrentHashMap的哈希数组Node[]
  • 原子类例如AtomicIntegervalue属性
  • 多个线程操作同一块内存的同一个变量(保证有序性和内存可见性,有序性和可见性同等重要,都不能忽略)

 

 一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰 之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行读取时的可见性,即一个线程修改 了某个变量的值,这新值对其他线程来说是立即可见的。(volatile 解决了 线程间共享变量的可见性问题)。

   第一:使用 volatile 关键字会强制修改的值立即写入主存

   第二:使用 volatile 关键字的话,当线程 2 进行修改时,会导致线程 1 的 工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话,就是 CPU L1 或者 L2 缓存中对应的缓存行无效);

   第三:由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1 再次读取变量 stop 的值时会去主存读取。 那么,在线程 2 修改 stop 值时(当然这里包括 2 个操作,修改线程 2 工 作内存中的值,然后将修改后的值写入内存),会使得线程 1 的工作内存中缓 存变量 stop 的缓存行无效,然后线程 1 读取时,发现自己的缓存行无效,它会 等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。 那么线程 1 读取到的就是最新的正确的值。 

  2)禁止进行指令重排序,阻止编译器对代码的优化。

   volatile 关键字禁止指令重排序有两层意思:

    I) 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的 更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定 还没有进行;

    II) 在进行指令优化时,不能 volatile 变量前面的语句放在其后面执行, 也不能把 volatile 变量后面的语句放到其前面执行。 为了实现 volatile 的内存语义,加入 volatile 关键字时,编译器在生成字节码时, 会在指令序列中插入内存屏障,会多出一个 lock 前缀指令。

  内存屏障,有 2 个作用:1.于这个内存屏障的指令必须先执行,于这个内存屏障 的指令必须后执行。2.使得内存可见性。所以,如果你的字段是 volatile,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据。在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。

  lock 前缀指令在多核处理器下会引发了两件事情: 1.将当前处理器中这个变量所在缓存行的数据会写回到系统内存。这个写回内存的 操作会引起在其他 CPU 里缓存了该内存地址的数据无效。但是就算写回到内存,如果 其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了 保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总 线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内 存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进 行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。 2.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面 的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全 部完成。

 

  内存屏障可以被分为以下几种类型:

  LoadLoad 屏障:对于这样的语句 Load1; LoadLoad; Load2,在 Load2 及后续读取 操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕;

  StoreStore 屏障:对于这样的语句 Store1; StoreStore; Store2,在 Store2 及后续写 入操作执行前,保证 Store1 的写入操作对其它处理器可见;

  LoadStore 屏障:对于这样的语句 Load1; LoadStore; Store2,在 Store2 及后续写 入操作被刷出前,保证 Load1 要读取的数据被读取完毕;

  StoreLoad 屏障:对于这样的语句 Store1; StoreLoad; Load2,在 Load2 及后续所 有读取操作执行前,保证 Store1 的写入所有处理器可见;它的开销是四种屏障中最 大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功 能。

 

来源《Java并发编程实践》

happen-before(定义一些禁止编译优化的场景,保证并发编程的正确性) :

① 程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。
② 监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。
③ volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
④ 线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
⑤ 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
⑥ 中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
⑦ 终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
⑧ 传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C