1. Java内存模型的基础
1.1 : 并发编程模型的两个关键问题
-
线程之间如何通信及线程之间如何同步(指并发执行的活动实体的线程)
-
在命令式编程中,线程之间的通信机制有两种 : 共享内存和消息传递
1). 共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信
2). 消息传递并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信 -
同步是指程序中用于控制不同线程间操作发生相对顺序的机制,在共享内存的并发模型中,同步是显式进行的,在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的
-
java的并发采用的是共享的内存模型,java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明
1.2 : Java内存模型的抽象结构
- 在java中,所有实例域,静态域和数组元素都存储在堆内存中,堆内存在线程之间共享.局部变量,方法定义参数和异常处理器参数不会再线程之间共享,它们不会有内存可见性问题,也不受内存模型影响.
- java线程之间的通信有java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见
- JMM定义了线程和主内存之间的抽象关系 : 线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存存储了该线程已读/写共享变量的副本
- JMM通过控制主内存和每个线程之间的交互,来为java程序员提供内存可见性保证
1.3 : 从源码到指令序列的重排序
- 在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,分三种类型
1). 编译器优化的重排序.编译器在不改变单线程程序语义的前提下,可以重新排序语句的执行顺序
2). 指令级并行的重拍讯.现代处理器采用指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行.如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
3). 内存系统的重排序.由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作做看上去可能是乱序执行 - 最终实际执行的指令序列,会分别经历下面3种重排序:如下图所示
- 上述1属于编译器重排序.2,3属于处理器重排序.这些重排序可能会导致多线程程序出现内存可见性问题.对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序.对于处理器排序,JMM处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序
- JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,来提供一致的内存可见性保证
1.4 : 并发编程模型的分类
- 现代的处理器使用写缓冲区临时保存向内存写入的数据,写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟.同时通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用.每个处理器的写缓冲区只对它所在的处理器可见.
- 为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序.JMM把内存屏障指令分为4类:如下图所示
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1数据的装载先于Load2及所有后续装载指令的装载 |
StoreStore Barriers | Store1;StoreStore;Store2 | 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储 |
LoadStore Barriers | Load1;LoadStore;Store1 | 确保Load1数据装载先于Store2及所有后续存储指令刷新到内存 |
StoreLoad Barriers | Load2;LoadStore;Store2 | 确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载.SroteLoad Barriers 会使改屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令 |
1.5 happens-before简介
- 在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在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 - 两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作的执行结果对后一个操作可见,且前一个操作按顺序排在第二个操作之前
- happens-before 与 JMM的关系如下图所示
- 如上图所示,一个happens-before规则对应于一个或多个编译器和处理器重排序规则.
2. 重排序
- 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段
2.1 : 数据依赖性
- 如果两个操作访问同一个变量,且这2个操作中有一个是写操作,此时这两个操作之间就存在数据依赖性,数据依赖性分为3中类型,如下图所示:
名称 | 代码示例 | 说明 |
---|---|---|
写后续 | a = 1;b = a; | 写一个变量之后,再读这个变量 |
写后写 | a = 1;a = 2; | 写一个变量之后,再写这个变量 |
读后写 | a = b;b = 1; | 读一个变量之后,再写这个变量 |
- 编译器和处理器可能会对操作做重排序,编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序.但是,这里的数据依赖性指的是单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑
2.2 : as-if-serial语义
- as-if-serial语义的意思是 : 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变.编译器,runtime和处理器都必须遵守as-if-serial语义,为了遵守as-if-serial语义,编译器和处理器不会对存在的数据依赖性的操作做重排序.因为重排序会改变执行结果,反之,有可能被编译器和处理器做重排序
- 在单线程程序中无需担心重排序会干扰代码执行结果,也无需担心内存可见性问题
2.3 : 程序顺序规则
- 在计算机中,软件技术和硬件技术有一个共同的目标
:在不改变程序执行结果的前提下,尽可能提高并行度.编译器和处理器遵从这一目标,从happens-before的语义可以看出,JMM也同样遵从
2.4 : 重排序对多线程的影响
- 在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(as-if-serial语义).在多线程,对存在控制依赖性的操作重排序,可能会改变程序的执行结果
3. 顺序一致性
- 顺序一致性内存模型是一个理论参考模型,再设计的时候,处理器的内存模型和编程语言的内存模型会以顺序一致性模型作为参照
3.1 : 数据竞争与顺序一致性
- 当程序未正确同步时,就可能会存在数据竞争
- Java内存模型对数据竞争的定义
1). 在同一个线程中写一个变量,在另一个线程读同一个变量,而且读/写没有通过同步来排序 - 如果一个多线程程序能正确同步,就不会有数据竞争,程序的执行将具有顺序一致性
3.2 : 顺序一致性内存模型
顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证.它有两大特性 :
1). 一个线程中的所有操作必须按照程序的顺序来执行
2). (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序,在顺序一致性内存模型中,每一个操作都必须是原子执行且立刻对所有线程可见
如图所示 :
- 就是说,在顺序一致性模型中,这个内存可以通过一个左右摆动的开关来连接到任意一个线程,同时每一个线程都必须按照程序的顺序来执行内存读/写,在任意时间点只能有一个线程可以连接到内存,在多线程并发执行时,模型中的所有操作之间具有全序关系
3.3 : 同步程序的顺序一致性效果
- 在JMM中执行它可以在临界区内重排序,而按照顺序一致性模型中,按程序顺序执行
- JMM在具体实现上的基本方针 : 在不改变(正确同步)程序执行结果的前提下,尽可能的让编译器和处理器去做一个优化
3.4 : 未同步程序的执行特性
- 对于未同步或者未正确同步的多线程程序,JMM之提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,False),JMM保证线程读操作读取到的值不会无中生有的冒出来
- JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致.因为如果要保证执行结果一直,JMM需要禁止大量的处理器和编译器的优化,这对程序的执行性能会产生很大的影响
- 未同步程序在JMM中执行时,整体上是无序,无法预知执行结果,未同步程序在两个模型中的几个差异
1). 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序顺序执行
2). 顺序一致性模型保证所有线程之内看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序
3). JMM不保证对64位的long类型和dubbo类型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作具有原子性
4. volatile的内存语义
4.1 : volatile的特性
- 锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写
volatile自身的特性 : - 可见性 : 对一个volatile变量,对该变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
- 原子性 : 对任意单位volatile变量的读/写具有原子性,但类似于volatile++这种符合操作不具有原子性
4.2 : volatile写-读建立的happens-before关系
- 从JDK5开始.volatile变量的写-读可以实现线程之间的通信
- volatile写和锁的释放有相同的内存语义,volatile读和锁的获取有相同的内存语义
4.3 : volatile写-读的内存语义
- volatile写的内存语义 :当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
- volatile读的内存语义 : 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效.线程接下来将从主内存读取一个共享变量
- volatile写和读的内存语义总结:
1). 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(我要对共享变量做修改了)消息
2). 线程B第一个volatile变量时,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做的修改)消息
3). 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息
4.4 : volatile内存语义的实现
- volatile重排序规则表如下 :
是否重排序 | 第二个操作 | 第二个操作 | 第二个操作 |
---|---|---|---|
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
举例说明 : 就是说当第一个操作是普通读/写,第二个操作是volatile读时,编译器不能重排序这两个操作
- JMM内存屏障插入策略
1). 在每个volatile写操作的前面插入一个StoreStore屏障
2). 在每个volatile写操作的后面插入一个StoreLoad屏障
3). 在每个volatile读操作的后面插入一个LoadLoad屏障
4). 在每个volatile读操作的后面插入一个LoadStore屏障
4.5 : JSR-133为什么要增强volatile的内存语义
- 由于volatile仅仅保证单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性,在功能上锁更强大,在嗑伸缩性和执行性能上,volatile更有优势
5. 锁的内存语义
- 锁可以让临界区互斥执行
5.1 : 锁的释放-获取建立的happens-before关系
- 锁是Java并发编程中最重要的同步机制,锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息
- 代码:
class MonitorExample{
int a = 0;
public synchronized void writer(){ // 1
a++; // 2
} // 3
public synchronized void reader(){ // 4
int i =a; // 5
......
} // 6
}
1). 顺序次序规则 : 1 happens-before 2,2 happens-before 3,4 happens-before 5,5 happens-before 6
2). 根据监视器锁规则 : 3 happens-before 4
3). 根据happens-before的传递性 : 2 happens-before 5
5.2 : 锁的释放和获取的内存语义
- 当线程释放锁时,JMM会把改线程对应的本地内存中的共享变量刷新到主内存中
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量
- 对比锁释放-获取的内存语义与volatile写-读的内存语义可以看出 : 锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义
- 锁释放和锁获取的内存语义总结
1). 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出消息(线程A对共享变量所做的修改)
2). 线程B获取一个锁,实质上是线程B接收了某个线程发出的消息(在释放这个锁之前对共享变量所做的修改)
3). 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息
5.3 : 锁内存语义的实现
- 在ReentrantLock中条用lock()获取锁,unlock方法释放锁
- ReentrantLock分为公平锁和非公平锁
- 公平锁调用轨迹 :
1). ReentrantLock : lock()
2). FairSync : lock()
3). AbstractQueuedSynchronizer : acquire(int arg)
4). ReentrantLock : tryAcquire(int acquires) - 在第四步真正开始加锁,下面是该方法源码
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取锁的开始,受限读取volatile变量state
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
从源码可以看出,加锁放首先读volatile变量state
- 使用公平锁时,解锁方法unlock调用轨迹
1). ReentrantLock : unlock()
2). abstractQueuedSynchrinizer : release(int arg)
3). Sync : tryRelease(int releases) - 在第三步真正开始释放锁,源码如下
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 释放锁的最后,写volatile变量state
setState(c);
return free;
}
-
总结 : 公平锁在释放最后写volatile变量state,在获取锁时首先读这个volatile变量,根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变得对获取锁的线程可见
-
使用非公平锁时,加锁方法lock()的调用轨迹 :
1). ReentrantLock :lock()
2). NonfairSync : lock()
3). AbstractQueuedSynchronozer : compa
reAndSetState(int expect,int update) -
第三步真正加锁,源码如下
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
- 该方法以原子操作的方式更新state变量,JDK文档对该方法说明如下 :
如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值.此操作具有volatile读和写的内存语义 - 公平锁和非公平锁的内存语义总结 :
1). 公平锁和非公平锁释放时,最后都要写一个volatile变量state
2). 公平锁获取时,首先会去读volatile变量state
3). 非公平锁获取时,首先会用CAS(compareAndSet()方法)更新volatile变量,这个操作同时具有volatile读/写的内存语义
5.4 : concurent包的实现
- 由于Java的CAS同时具有volatile的读/写内存语义.因此Java线程之间通信有4中方式 :
1). A线程写volatile变量,随后B线程读这个volatile变量
2). A线程写volatile变量.随后B线程用CAS更新这个volatile变量
3). A线程用CAS更新一个volatile变量.随后B线程用CAS更新这个volatile变量
4). A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量 - concurrent包的一个通用化的实现模式
1). 声明共享变量为volatile
2). 使用CAS的原子条件来实现线程之间的同步
3). 配合volatile的读/写和CAS所具有的volatile读和写的内存语义来线程之间的通信 - AQS,非阻塞数据结构和原子变量类(Java.util.concurrent.atomic包中的类),这些concurrent保重的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现,看下图concurrent实现示意图 :
6. final锁的内存语义
6.1 : final域的重排序规则
- 对于fina域,编译器和处理器遵守两个重排序规则 :
1). 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
2). 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能被重排序