天天看点

happens-before,双重检查锁定与延迟初始化,Java内存模型综述--java并发编程的艺术(五)JMM的设计双重检查锁定与延迟初始化Java内存模型综述

  • JMM的设计
    • happens-before的定义
    • happens-before规则
  • 双重检查锁定与延迟初始化
    • 双重检查锁定的由来
    • 基于volatile的解决方案
    • 基于类初始化的解决方案
  • Java内存模型综述
    • 处理器的内存模型
    • 各种内存模型之间的关系
    • JMM的内存可见性保证

JMM的设计

从JMM设计者的角度,在设计JMM时,需要考虑两个关键因素

1)程序员对内存模型的使用。程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。

2)编译器和处理器对内存模型的实现。编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。

JMM把happens-before要求禁止的重排序分为了下面两类

1)会改变程序执行结果的重排序。

对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。

2)不会改变程序执行结果的重排序。

对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)。

happens-before,双重检查锁定与延迟初始化,Java内存模型综述--java并发编程的艺术(五)JMM的设计双重检查锁定与延迟初始化Java内存模型综述

JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在。

JMM对编译器和处理器的束缚已经尽可能少。JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。

happens-before的定义

happens-before关系的定义如下

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作

可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照

happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系

来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。

happens-before规则

1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。

2)监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。

3)volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

5)start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

6)join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

双重检查锁定与延迟初始化

双重检查锁定的由来

在Java程序中,有时候可能需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。在早期的JVM中,synchronized(甚至是无竞争的synchronized)存在巨大的性能开销。因此,人们想出了一个“聪明”的技巧:双重检查锁定(Double-Checked Locking)。人们想通过双重检查

锁定来降低同步的开销。

基于volatile的解决方案

happens-before,双重检查锁定与延迟初始化,Java内存模型综述--java并发编程的艺术(五)JMM的设计双重检查锁定与延迟初始化Java内存模型综述

这个方案本质上是通过禁止图中的2和3之间的重排序,来保证线程安全的延迟初始化。

基于类初始化的解决方案

VM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案

Java内存模型综述

处理器的内存模型

顺序一致性内存模型是一个理论参考模型,JMM和处理器内存模型在设计时通常会以顺序一致性内存模型为参照。

根据对不同类型的读/写操作组合的执行顺序的放松,可以把常见处理器的内存模型划分为如下几种类型。

1)放松程序中写-读操作的顺序,由此产生了Total Store Ordering内存模型(简称为TSO)。

2)在上面的基础上,继续放松程序中写-写操作的顺序,由此产生了Partial Store Order内存模型(简称为PSO)。

3)在前面两条的基础上,继续放松程序中读-写和读-读操作的顺序,由此产生了RelaxedMemory Order内存模型(简称为RMO)和PowerPC内存模型

happens-before,双重检查锁定与延迟初始化,Java内存模型综述--java并发编程的艺术(五)JMM的设计双重检查锁定与延迟初始化Java内存模型综述

所有处理器内存模型都允许写-读重排序,它们都使用了写缓存区。写缓存区可能导致写-读操作重排序。同时,我们可以看到这些处理器内存模型都允许更早读到当前处理器的写,原因同样是因为写缓存区。由于写缓存区仅对当前处理器可见,这个特性导致当前处理器可以比其他处理器先看到临时保存在自己写缓存区中的写。

表中的各种处理器内存模型,从上到下,模型由强变弱。越是追求性能的处理器,内存模型设计得会越弱。因为这些处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。

JMM屏蔽了不同处理器内存模型的差异,它在不同的处理器平台之上为Java程序员呈现了一个一致的内存模型。

各种内存模型之间的关系

JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。

happens-before,双重检查锁定与延迟初始化,Java内存模型综述--java并发编程的艺术(五)JMM的设计双重检查锁定与延迟初始化Java内存模型综述

常见的4种处理器内存模型比常用的3中语言内存模型要弱,处理器内存模型和语言内存模型都比顺序一致性内存模型要弱。同处理器内存模型一样,越是追求执行性能的语言,内存模型设计得会越弱。

JMM的内存可见性保证

1)单线程程序。单线程程序不会出现内存可见性问题。编译器、runtime和处理器会共同确

保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。

2)正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行

结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限

制编译器和处理器的重排序来为程序员提供内存可见性保证。

3)未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取

到的值,要么是之前某个线程写入的值,要么是默认值(0、null、false)。

happens-before,双重检查锁定与延迟初始化,Java内存模型综述--java并发编程的艺术(五)JMM的设计双重检查锁定与延迟初始化Java内存模型综述

把前一周看的书总结了一下午加半晚上,大致明白了很多概念,对于并发编程才是刚刚起步,加油!