天天看点

Java内存模型

Java 内存模型

Java 内存模型是通过各种操作来定义的,包括对变量的读/写操作,监视器的加锁、解锁操作,以及线程的启动和合并操作。 <code>JMM</code> 为程序中所有的操作定义了一个偏序关系,称之为 <code>Happens-Before</code>。如果想要保证执行 B 操作的线程看到操作 A 的结果(无论 A 和 B 是否在同一个线程中执行),那么 A 和 B 之间的操作必须满足 <code>Happens-Before</code> 的关系。如果两个操作之间缺少 <code>Happens-Before</code> 关系,那么 <code>JVM</code> 就可以对它们进行任意的重排序。

程序顺序规则:如果程序中操作 A 在操作 B 之前,那么在线程中操作 A 将在 操作 B 之前执行

监视器锁规则:在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行(显示锁和内置锁在加锁和解锁等操作上有相同的内存语义)

volatile 变量规则:对 volatile 变量的写入操作必须在对该变量的读操作之前执行(原子变量与 volatile 变量在读操作和写操作上有着相同的语义)

线程启动规则:在线程上对 <code>Thread.start()</code> 的调用必须在该线程中执行任何操作之前执行

线程结束规则:线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从 <code>Thread.join()</code> 中成功返回,或者在调用 <code>Thread.isAlive()</code> 中返回 <code>false</code>

中断规则:当一个线程在另一个线程上调用 <code>interrupt</code> 时,必须在被中断线程检测到 <code>interrupt</code> 之前执行(或者抛出 <code>InterruptException</code>,或者调用 <code>isInterrupted</code> 和 <code>interrupted</code>)

终结器规则:对象的构造函数必须在启动该对象的终结器之前执行

传递性:如果操作 A 在操作 B 之前执行,并且 B操作在 C操作之前执行,那么操作 A 必须在 操作 C 之前执行

线程 A 释放了一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改)的消息

线程 B 获取了一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改)的消息

线程 A 释放锁,随后线程 B 获得了这个锁,这个过程实质上是线程 A 通过主存向线程 B 发送了消息

线程 A 写一个 <code>volatile</code> 变量,实质上是线程 A 向接下来将要读这个 <code>volatile</code> 变量的某个线程发送了对共享变量所做修改的消息

线程 B 读取一个 <code>volatile</code> 变量,实质上是线程 B 接收了之前某个线程发出的在读这个 <code>volatile</code> 变量之前所做修改的消息

线程 A 写一个 <code>volatile</code> 变量,随后线程 B 读取了这个变量,这个过程实质上是线程 A 通过主存向线程 B 发送了修改这个共享变量的消息

内存屏障

为了实现 <code>volatile</code> 的内存语义,编译器会在生成字节码时,在指令序列中插入内存屏障来禁止特定类型的处理器重排序

内存屏障是一种 barrier 指令类型,它导致 CPU 或编译器对 barrier 指令前后发出的内存操作执行顺序约束。也就是说,在 barrier 之前的内存操作保证在 barrier 之后的操作之前执行

内存屏障主要分为以下四种:

<code>LoadLoad</code>内存屏障:对于这样的语句 <code>load1;LoadLoad;load2</code>,在 <code>load2</code> 及后续读取操作要读取的数据被访问之前,保证 <code>load1</code> 要读取的数据被读取完毕

<code>StoreStore</code>内存屏障:对于这样的语句 <code>store1;StoreStore;store2</code>,在 <code>store2</code> 及后续的写入操作执行之前,保证 <code>store1</code> 中的写入操作对处理器可见

<code>LoadStore</code>内存屏障:对于这样的语句 <code>load1;LoadStore;store1</code>,在 <code>store1</code> 及后续写入操作被刷出之前,保证 <code>load1</code> 的读取操作要全部完成

<code>StoreLoad</code>内存屏障:对于这样的语句 <code>store1;StoreLoad;load1</code>,在<code>load1</code> 及后续的所有读取操作执行之前,保证 <code>store1</code> 中的数据写入对于所有处理器可见。这个内存屏障是所有内存屏障中开销最大的,这个屏障是一个万能屏障,兼具其他三种内存屏障的功能

<code>Java</code> 中 <code>volatile</code> 的实现

对每个<code>volatile</code> 写操作之前插入一个 <code>StoreStore</code> 内存屏障

对每个 <code>volatile</code> 写操作之后插入一个<code>StoreLoad</code> 内存屏障

对每个 <code>volatile</code>读操作之前插入一个 <code>LoadLoad</code> 内存屏障

对每个 <code>volatile</code> 读操作之后插入一个 <code>LoadStore</code> 内存屏障

在构造函数内对一个 <code>final</code> 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

初次读一个包含 <code>final</code> 域的对象的引用,与随后初次读这个 <code>final</code> 域,这两个操作之间不能重排序

写 <code>final</code> 域的重排序规则禁止把 <code>final</code> 域的写重排序到构造函数之外,这个规则的实现包含下面两个方面:

<code>JMM</code> 禁止编译器把 <code>final</code> 域的写重排序到构造函数之外

编译器会在 <code>final</code> 域的写之后,构造函数的 <code>return</code> 之前,插入一个 <code>StoreStore</code> 内存屏障。这个屏障禁止处理器把 <code>final</code> 域的写重排序到构造函数之外。写 <code>final</code> 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 <code>final</code> 域已经被正确初始化过了,而普通域则不具备这个保障

在一个线程中,初次读对象引用和初次读该对象包含的 <code>final</code> 域,<code>JMM</code> 禁止处理器重排序这两个操作(注意,仅仅只是针对处理器)

编译器会在读 <code>final</code> 域操作前插入一个 <code>LoadLoad</code> 内存屏障

初次读对象引用与初次读该对象包含的 <code>final</code> 域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器也不会重排序这两个操作

大多数处理器也会遵守间接依赖也不会重排序这两个操作,但是少数处理器允许存在间接依赖关系的操作做重排序,这个规则就是针对这些处理器的。

读 <code>final</code> 域的重排序规则可以确保:在读一个 <code>final</code> 域之前,一定会先读包含这个 <code>final</code> 域的引用

对于引用类型,写 <code>final</code> 域的重排序规则对编译器和处理器增加了如下约束:在构造函数内对一个 <code>final</code> 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。这一规则确保了其它线程能够读到被正确初始化的 <code>final</code> 引用对象的成员域

静态工厂方法实现单例模式

枚举类型实现单例模式

延迟初始化类

<code>DCL</code>(双重检查锁)

​ 实际上,一般来讲,正常地使用饿汉式地方式来实现单例是最好的解决方案。但是如果确实需要使用延迟化的加载方式,如果需要使用到静态变量,那么使用延迟化初始化类的方式实现是最好的;如果不得不使用一个对象的字段来表示单例,那么就使用 <code>DCL</code> 的方式。

继续阅读