[转载自并发编程网 – ifeve.com 原文链接:http://ifeve.com/tag/jmm/]
JMM 掌管着一个线程对内存的动作 (读和写)影响其他线程对内存的动作的方式。由于使用处理器寄存器和预处理 cache 来提高内存访问速度带来的性能提升,Java 语言规范(JLS)允许一些内存操作并不对于所有其他线程立即可见。有两种语言机制可用于保证跨线程内存操作的一致性――synchronized 和 volatile。
按照 JLS 的说法,“在没有显式同步的情况下,一个实现可以自由地更新主存,更新时所采取的顺序可能是出人意料的。”其意思是说,如果没有同步的话,在一个给定线程中某种顺序的写操作对于另外一个不同的线程来说可能呈现出不同的顺序, 并且对内存变量的更新从一个线程传播到另外一个线程的时间是不可预测的。
虽然使用同步最常见的原因是保证对代码关键部分的原子访问,但实际上同步提供三个独立的功能――原子性、可见性和顺序性。
原子性非常简单――同步实施一个可重入的(reentrant)互斥,防止多于一个的线程同时执行由一个给定的监视器保护的代码块。不幸的是,多数文章都只关注原子性方面,而忽略了其他方面。
但是同步在 JMM 中也扮演着很重要的角色,会引起 JVM 在获得和释放监视器的时候执行内存壁垒(memory barrier)。
一个线程在获得一个监视器之后,它执行一个读屏障(read barrier)――使得缓存在线程局部内存(比如说处理器缓存或者处理器寄存器)中的所有变量都失效,这样就会导致处理器重新从主存中读取同步代码块使用的变量。与此类似,在释放监视器时,线程会执行一个写屏障(write barrier)――将所有修改过的变量写回主存。
互斥独占和内存壁垒结合使用意味着只要您在程序设计的时候遵循正确的同步法则(也就是说,每当写一个后面可能被其他线程访问的变量,或者读取一个可能最后被另一个线程修改的变量时,都要使用同步),每个线程都会得到它所使用的共享变量的正确的值。
在有些情况下,比如说在像ConcurrentHashMap 之类的一些使用非常广泛的库类中,在开发过程当中还需要一些额外的专业技能和努力(可能比一般的开发要多出很多倍)来获得较高的性能。
顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会把顺序一致性内存模型作为参照。JMM和处理器内存模型在设计时会对顺序一致性模型做一些放松,因为如果完全按照顺序一致性模型来实现处理器和JMM,那么很多的处理器和编译器优化都要被禁止,这对执行性能将会有很大的影响。
根据对不同类型读/写操作组合的执行顺序的放松,可以把常见处理器的内存模型划分为下面几种类型:
注意,这里处理器对读/写操作的放松,是以两个操作之间不存在数据依赖性为前提的(因为处理器要遵守as-if-serial语义,处理器不会对存在数据依赖性的两个内存操作做重排序)。
下面的表格展示了常见处理器内存模型的细节特征:
内存模型名称 | 对应的处理器 | Store-Load 重排序 | Store-Store重排序 | Load-Load 和Load-Store重排序 | 可以更早读取到其它处理器的写 | 可以更早读取到当前处理器的写 |
TSO | sparc-TSOX64 | Y | Y | |||
PSO | sparc-PSO | Y | Y | Y | ||
RMO | ia64 | Y | Y | Y | Y | |
PowerPC | PowerPC | Y | Y | Y | Y | Y |
在这个表格中,我们可以看到所有处理器内存模型都允许写-读重排序,原因在第一章以说明过:它们都使用了写缓存区,写缓存区可能导致写-读操作重排序。同时,我们可以看到这些处理器内存模型都允许更早读到当前处理器的写,原因同样是因为写缓存区:由于写缓存区仅对当前处理器可见,这个特性导致当前处理器可以比其他处理器先看到临时保存在自己的写缓存区中的写。
上面表格中的各种处理器内存模型,从上到下,模型由强变弱。越是追求性能的处理器,内存模型设计的会越弱。因为这些处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。
由于常见的处理器内存模型比JMM要弱,java编译器在生成字节码时,会在执行指令序列的适当位置插入内存屏障来限制处理器的重排序。同时,由于各种处理器内存模型的强弱并不相同,为了在不同的处理器平台向程序员展示一个一致的内存模型,JMM在不同的处理器中需要插入的内存屏障的数量和种类也不相同。下图展示了JMM在不同处理器内存模型中需要插入的内存屏障的示意图:
如上图所示,JMM屏蔽了不同处理器内存模型的差异,它在不同的处理器平台之上为java程序员呈现了一个一致的内存模型。
JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。下面是语言内存模型,处理器内存模型和顺序一致性内存模型的强弱对比示意图:
从上图我们可以看出:常见的4种处理器内存模型比常用的3中语言内存模型要弱,处理器内存模型和语言内存模型都比顺序一致性内存模型要弱。同处理器内存模型一样,越是追求执行性能的语言,内存模型设计的会越弱。
从JMM设计者的角度来说,在设计JMM时,需要考虑两个关键因素:
由于这两个因素互相矛盾,所以JSR-133专家组在设计JMM时的核心目标就是找到一个好的平衡点:一方面要为程序员提供足够强的内存可见性保证;另一方面,对编译器和处理器的限制要尽可能的放松。下面让我们看看JSR-133是如何实现这一目标的。
为了具体说明,请看前面提到过的计算圆面积的示例代码:
double pi = 3.14; //A double r = 1.0; //B double area = pi * r * r; //C
上面计算圆的面积的示例代码存在三个happens- before关系:
由于A happens- before B,happens- before的定义会要求:A操作执行的结果要对B可见,且A操作的执行顺序排在B操作之前。 但是从程序语义的角度来说,对A和B做重排序即不会改变程序的执行结果,也还能提高程序的执行性能(允许这种重排序减少了对编译器和处理器优化的束缚)。也就是说,上面这3个happens- before关系中,虽然2和3是必需要的,但1是不必要的。因此,JMM把happens- before要求禁止的重排序分为了下面两类:
JMM对这两种不同性质的重排序,采取了不同的策略:
下面是JMM的设计示意图:
从上图可以看出两点:
Java程序的内存可见性保证按程序类型可以分为下列三类:
下图展示了这三类程序在JMM中与在顺序一致性内存模型中的执行结果的异同:
只要多线程程序是正确同步的,JMM保证该程序在任意的处理器平台上的执行结果,与该程序在顺序一致性内存模型中的执行结果一致。
JSR-133对JDK5之前的旧内存模型的修补主要有两个:
程晓明,Java软件工程师,国家认证的系统分析师、信息项目管理师。专注于并发编程,个人邮箱:[email protected]。