java内存模型
Java内存模型的基础
并发编程模型的两个关键性问题
线程之间如何通信和线程之间如何同步?
通信是指线程之间通过何种机制来交换信息?通信的机制有两种,共享内存和消息传递
同步是指程序中用于控制不同线程间操作生相对顺序的机制。
java并发采用的是共享内存模型。线程通信总是隐式进行,同步是显式的。
java内存模型(JMM)的抽象结构
java中所有的实例对象,静态对象和数组都存储在堆内存。堆内存在线程中共享。局部变量,方法定义参数,异常处理参数不会在线程中共享。
线程间的共享变量存储在主内存中,每个线程都有自己的本地内存,存储了该线程以读/写共享变量的副本。
线程A和线程B要通信要经历2个步骤:
- 线程A把本地内存副本刷新到主内存中
- 线程B去读取主内存中的共享变量
重排序
- 编译器优化的重排序
- 指令级并行的重排序
- 内存系统的重排序
由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。为了保证内存可见性,java在生成指令的适当位置插入内存屏障指令来禁止特定类型的处理起重排序。
happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么两个操作之间必须存在happens-before关系。
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
- volatile变量规则:对一个volatile的写,happens-before 于任意后续对这个volatile的读
- 传递性:A happens-before B,B happens-before C,则A happens-before C
- 注意:两个操作具有happens-before的关系,并不代表前一个操作必须在后一个操作前执行。仅仅要求前一个操作对后一个操作可见,且前一个操作按顺序排在第二个之前。
重排序
重排序是指编译器和处理起为了优化程序性能而对指令进行重新排序的一种手段。
数据依赖性
如果两个操作访问一个变量,并且其中一个操作为写操作,就说操作间存在依赖性。
名称 | 代码 |
---|---|
写后读 | a=1;b=a; |
写后写 | a=1;a=2; |
读后写 | a=b;b=1; |
只要重排序,程序的执行结果就会发生变化。
as-if-serial 语义
不管怎么重排序,单线程的程序执行结果不能发生改变。
as-if-serial
语义是单线程无需担心重排序会干扰他们,也无需担心内存可见性的问题。
重排序对多线程的影响
int a = 0;
boolean flag = false;
private void writer() {
a = 1 ; //1
flag = true; //2
}
private void reader() {
if (flag) { // 3
int i = a * a; // 4
}
}
假设A线程执行writer,B线程执行reader,那么B在执行时,可否看见A对a变量的操作?
当然是不一定的。操作1和操作2没有数据依赖关系,可以进行重排序,操作3和操作4也没有数据依赖关系,也可重排序。
顺序一致性
什么是数据竞争:在一个线程中写一个变量,在另一个线程中读同一个变量,而且写和读没有通过同步来排序。此时程序的执行往往结果不正确。
如果程序是正确同步的,则称之为顺序一致性(理想化模型),同步指对常用同步原语(synchronized,volatile,final 的正确运用)
顺序一致性内存模型有两大特性:
- 一个线程的所有操作必须按照程序的顺序来执行
- 所有线程都只能看到一个单一的操作执行顺序。每个操作都必须原子执行并对所有线程可见。
volatile的内存语义
volatile是可见的,所以满足happens-before原则
volatile拥有的特性:
- 可见性。对于volatile的读,总是能看到(任意线程)对这个volatile变量最后的写
- 原子性。对于任意单个volatile变量的读/写具有原子性,但是复合操作不具备原子性
volatile的写和锁的释放有相同的内存语义,volatile的读与锁的获取有相同的语义。
int a = 0;
volatile boolean flag = false;
private void writer() {
a = 1 ; // 1
flag = true; // 2
}
private void reader() {
if (flag) { // 3
int i = a * a; // 4
}
}
happens-before 的关系:
- 1 happens-before 2
- 2 happens-before 3
- 1 happens-before 4
volatile的写-读内存语义
当写一个volatile变量时,JMM会将该线程的本地内存刷新到主内存中
当读一个volatile变量时,JMM会将该线程对象的本地内存置为无效,线程从主内存中获取共享变量
volatile内存语义的实现
JMM通过限制重排序类型来实现volatile
是否能重排序 | 第二个操作 | 第二个操作 | 第二个操作 |
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | No | NO |
锁的内存语义
锁可以让临界区互斥执行。还可以让释放锁的线程向获得同一个锁的线程发送消息。
int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
}
假设A线程执行writer,随后B线程执行reader
happens-before 的关系
- 1 happens-before 2 2 happens-before 3 ;4 happens-before 5 5 happens-before 6
- 根据监视器锁规则,3 happens-before 4
锁的释放和获得的内存语义
释放锁时,JMM会将该线程对应的本地内存中的共享变量刷新到主内存中。
获取锁时,JMM会将该线程的本地内存置为无效,在从主内存中获取共享变量。
锁内存的语义实现
int a = 0;
ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock();
try {
a ++;
}finally {
lock.unlock();
}
}
public void reader() {
lock.lock();
try {
int i = a;
}finally {
lock.unlock();
}
}
ReentrantLock的实现主要依赖于java同步器框架AbstractQueuedSynchronizer
AQS使用一个整形的volatile变量来维护状态:state变量
ReentrantLock分为公平锁和非公平锁
公平锁,加锁调用轨迹:
ReentrantLock:lock()
FairSync:lock()
AbstractQueuedSynchronizer:acquire()
ReentrantLock:tryAcquire(int acquires)
公平锁在释放锁时写volatile变量state,在获取锁时读这个变量。
非公平锁,非公平锁的释放和公平锁一样,这里看加锁的调用轨迹:
ReentrantLock:lock()
NonFairSync:lock()
AbstractQueuedSynchronizer:acquire.compareAndSetState(int expect,int update)
protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
该方法以原子操作更新state变量,即cas,cas具有volatile读和写的内存语义。
总结一下:
- 公平锁和非公平锁在释放时,都需要写一个volatile变量state
- 公平锁获取时,首先会去读volatile变量
- 非公平锁获取时,首先使用CAS 更新volatile变量。
锁的释放-获取有以下两种实现方式
- 利用volatile的读-写所具有的内存语义
- 利用CAS所附带的volatile读和volatile写语义
concurrent包的实现
concurrent包实现的通用化模式:
- 先声明一个共享变量volatile
- 然后使用CAS的原子条件更新来实现线程之间的同步
- 配合以volatile的读写和CAS来实现线程之间的通信
总结以下:
- volatile的读写和CAS是基础
- AQS,非阻塞数据队列,原子变量类基于此
- Lock,同步器,阻塞队列,Executor,并发容器又基于上面的
final的内存语义
final的读写更像是对普通变量的访问
final的重排序
两个规则:
在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,两个操作之间不能重排序
初次读一个包含final域的引用,与随后初次读这个final域,两个操作不能重排序
int i; // 普通变量 final int j; // final 变量 static FinalSample obj; public FinalSample() { //构造 i = 1; j = 2; } public static void writer() { // 写程序A执行 obj = new FinalSample(); } public static void reader() { // 读程序B执行 FinalSample object = obj; //读对象引用 int a = obj.i; // 读普通域 int b = obj.j; //读final域 }
写final重排序禁止把final域的写重排序到构造函数之外。
读final域的重排序规则可以确保在读一个对象的final域之前,一定会先去读包含这个final域的对象的引用。
只要对象是正确构造的(即没有“逸出”),那么不需要使用同步(lock和volatile)就可以保证任意线程都能看到这个final域在构造函数中初始化之后的值
happens-before
理解happens-before是理解JMM的关键。
定义如下:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果对第二个操作可见,而且第一个操作的执行顺序在第二个之前。
- 如果重排序对执行结果一致,那么允许这种重排序。
happens-before规则
- 程序顺序规则:一个线程中的每个操作,happens-before该线程之后的每一个操作
- 监视器锁规则:一个锁的解锁,happens-before之后对这个锁的加锁
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
- 传递性
- start规则:如果线程A操作ThreadB.start(),那么A线程的start操作happens-before B线程的任意操作
- join规则:如果线程A操作TheadB.join(),那么线程B中的任意操作happens-before于线程A操作join()。