Java内存模型-基础

Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。

如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步

什么是重排序?

通过之前的处理器缓存介绍,我们知道处理器不直接操作内存的数据,而是通过把内存的数据加载到对应地址的缓存行中,而CPU的执行速度远远大于内存加载到缓存的速度,这就存在内存访问延迟导致缓存未命中的问题。具体见Java内存模型-伪共享和缓存行填充
Java即时编译器(JIT)和CPU避免内存访问延迟最常见的技术是将指令管道化,然后尽量重排这些管道的执行以最大化利用缓存,从而把因为缓存未命中引起的延迟降到最小,这样可以明显提高CPU的执行效率。

as-if-serial语义

虽然JIT和CPU都可能进行指令重排序,但是需要在一定规则下才行,这就是as-if-serial原则。
具体语义是所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。

Happens-before原则

因为缓存中的数据与主内存的数据并不是实时同步的,且各CPU(或CPU核心)间缓存的数据也不是实时同步的。这导致在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。从程序的视角来看,就是在同一个时间点,各个线程所看到的共享变量的值可能是不一致的,这就是我们常说的内存可见性问题。

为此JSR制定了内存模型,其中有一个非常重要的原则:Happens-before
具体语义是Happens-before的前后两个操作不会被重排序且后者对前者的内存可见。

其中有几个重要的规则(其实我也记不住!!!):
1.程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作 B,其中,在程序中,所有的动作B都能出现在A之后。
2.监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视 器锁的加锁。
3.volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
4.线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
5.线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
6.中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
7.终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
8.传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C

内存屏障

内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题

内存屏障有两个能力:
1.阻止屏障两边的指令重排序
2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

JMM中具体体现在一些关键的地方,比如volatile、final、同步块出入口等会插入一些内存屏障禁止重排序,并保证happens-before原则从而实现了内存可见性。
(这些对于Java程序员是屏蔽的,所以Java程序员还是比较幸福滴~~~)

整体而言分可以将内存屏障为如下几种

  1. lfence是一种Load Barrier 读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
  2. sfence是一种Store Barrier 写屏障,能让写入缓存的最新数据写回到主内存
  3. mfence是一种全能型的屏障,具备ifence和sfence的能力
  4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。(具体介绍见Java内存模型-volatile语义和实现原理)

按照功能可以将内存屏障分为以下几种类型

1.LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
2.StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
3.LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
4.StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

Java内存模型-基础_第1张图片
内存屏障插入规则.png

为了保证final字段的特殊语义,也会在下面的语句加入内存屏障。
x.finalField = v; StoreStore; sharedRef = x;

总结

1、JIT和处理器为了降低缓存未命中率以提高执行效率,可能会对指令进行重排序
2、指令重排序需要满足as-if-serial规则,即保证它们重排序后的结果和程序代码本身的应有结果是一致的。
3、由于存在重排序、缓存和主存无法实时一致,可能导致多线程下内存不可见的问题,为此Java内存模型制定了一个重要的原则保证内存可见性:happens-before原则
4、Java为了解决内存可见性问题,提供了比如volatile、final等机制,这些机制都依赖于CPU级别的内存屏障指令,包括store屏障、load屏障、mfence屏障以及Lock锁
5、内存屏障的作用:
1)阻止屏障两边的指令重排序
2)强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

你可能感兴趣的:(Java内存模型-基础)