JVM-Java内存模型-学习笔记
1.内存模型的抽象
Java线程之间的通信由Java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。
Java内存模型的抽象示意图如下:
11.png
-
线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
- 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 线程B到主内存中去读取线程A之前已更新过的共享变量。
2.重排序
为了提升性能,编译器和处理器会对指令进行重排序:
1.编译器优化重排序
2.指令级重排序
3.内存系统的重排序
-
从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
33.png -
JMM属于语言级的内存模型,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
-
as-if-serial语义
- 不管怎么重排序,程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
- 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,但是会对不存在数据依赖关系的操作进行编译器和处理器重排序。
- as-if-serial语义把单线程程序保护了起来,单线程程序是按程序的顺序来执行的。无需担心重排序会干扰他们,也无需担心内存可见性问题。
3.处理器重排序与内存屏障指令
现代的处理器使用写缓冲区来临时保存向内存写入的数据。每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!
-
示例:
44.png- 处理器A和处理器B可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到x = y = 0的结果。
- 从内存操作实际发生的顺序来看,直到处理器A执行A3来刷新自己的写缓存区,写操作A1才算真正执行了。虽然处理器A执行内存操作的顺序为:A1->A2,但内存操作实际发生的顺序却是:A2->A1。此时,处理器A的内存操作顺序被重排序了。
4.volatile
- 由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而监视器锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。
- 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但复合操作不具有原子性(volatile++)。
-
volatile写-读建立的happens before关系
1. 从内存语义的角度来说,volatile与监视器锁有相同的效果:volatile写和监视器的释放有相同的内存语义;volatile读与监视器的获取有相同的内存语义。 示例代码 -
volatile写-读的内存语义
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。 2.png
2.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
3.png
3.写-读的内存语义总结
- 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了消息。
- .线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的消息。
- 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
-
volatile内存语义的实现
为了实现volatile内存语义,JMM会分别限制编译器重排序和处理器重排序。
- volatile重排序规则
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
- 基于保守策略的JMM内存屏障插入策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
- volatile写插入内存屏障后生成的指令序列示意图:
- volatile重排序规则
5.锁
java并发编程中最重要的同步机制
- 锁的释放-获取建立的happens before 关系(线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见)
``` class MonitorExample {//A执行writer()方法,随后线程B执行reader()方法
int a = 0;
public synchronized void writer() { //1
a++; //2
} //3
public synchronized void reader() { //4
int i = a; //5
……
} //6
}
-
锁释放和获取的内存语义
- 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了消息。
- 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的消息。
- 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
- 锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。
-
锁内存语义的实现(需要重看)
在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。ReentrantLock的实现依赖于java同步器框架AQS。AQS使用一个整型的volatile变量来维护同步状态。
- ReentrantLock分为公平锁和非公平锁
- 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
- 公平锁获取时,首先会去读这个volatile变量。
- 非公平锁获取时,首先会用CAS更新这个volatile变量,这个操作同时具有volatile读和volatile写的内存语义。
- ReentrantLock分为公平锁和非公平锁
-
concurrent包的实现
- 通用化的实现模式:
- 首先,声明共享变量为volatile;
- 然后,使用CAS的原子条件更新来实现线程之间的同步;
- 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
-
示意图
55.png
- 通用化的实现模式:
6.final
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
- 写final域的重排序规则
- 写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。
- 读final域的重排序规则
- 读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。
- final域是引用类型
- 在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
- final引用不能从构造函数内“逸出”
- 在构造函数内部,不能让这个被构造对象的引用为其他线程可见。
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample () {
i = 1; //1写final域
obj = this; //2 this引用在此“逸出”
//1和2可能进行重排序,那么如果先拿到2可能final域还没构造完成,final域被初始化后的值
}
}