- 主内存和工作内存
- 所有的变量都存储在主内存中,每个线程还有自己的工作内存
- 工作内存存储了主内存中数据的副本
- 线程对变量的操作必须在工作内存中进行,不能直接操作主内存中的变量
- 不同线程间不能直接访问对方的工作内存的变量
- 线程间变量的传递需要通过主内存来完成
- 内存间的交互
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
这里的read和load、store和write是成对的操作,必须按顺序执行,但不一定连续执行。关于这些操作,Java内存模型做出了一些规定:
- 不允许read和load、store和write操作之一单独出现
- 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- 一个变量在同一时刻只允许一条线成对其进行lock操作,lock和unlock必须成对出现
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
- 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- 对一个变量执行unlock操作之前,必须先把次变量同步到主内存中(执行store和write操作)。
- 为了提高执行性能,编译器和处理器经常会进行指令重排序,分为以下三种类型:
- 编译器优化的重排序。编译器不改变单线程语意的前提下对语句的重排序。
- 指令并行重排序。处理器在不不存数据依赖性的前提下,对语句对应的机器指令进行重排序。
- 内存系统重排序。由于缓存和读写缓冲区的存在,加载和存储操作可能是乱序执行的。
- Java内存模型对正确同步的多线程程序的内存一致性做了如下保证:
- 如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)--即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同(马上我们将会看到,这对于程序员来说是一个极强的保证)。这里的同步是指广义上的同步,包括对常用同步原语(lock,volatile和final)的正确使用。
- 一个线程内的所有操作必须按顺序执行
- (不管是否同步)所有线程都只能看到一个单一的操作执行顺序。
- 所有操作都必须是原子性的,且立即对其他线程可见。
- 为保证内存的可见性,Java编译器在生成指令序列的适当位置会加入内存屏障来禁止一些特定类型的重排序。这些内存屏障分为以下四种:
- 从JDK5开始,java使用新的JSR -133内存模型。JSR-133提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
与程序员密切相关的happens-before规则如下:
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
- 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
- 线程上调用start()方法happens before这个线程启动后的任何操作。
- 一个线程中所有的操作都happens before从这个线程join()方法成功返回的任何其他线程
请看下面使用volatile变量的示例代码:
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a; //4
……
}
}
}
假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before 关系可以分为两类:
1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
2. 根据volatile规则,2 happens before 3。3. 根据happens before 的传递性规则,1 happens before 4。Java内存模型是怎样实现volatile规则的呢?前面提到过编译器和处理器两种级别的重排序,java内存模型分别对这两种类型的重排序做了限制:
- 编译器限制
是否能重排序 第二个操作 第一个操作 普通读/写 volatile读 volatile写 普通读/写
NO volatile读 NO NO NO volatile写
NO NO
举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读写,第二个操作为volatile变量的写时,不允许重排序。以此类推。
volatile可以视为更轻量级的线程间通信机制,虽然它只提供变量单一操作的原子性,功能不如监视器强大,但是他更灵活,执行效率也更高。在一些特定的情况,可以使用它来代替监视器锁。
- 为了实现这些限制,编译器是生成字节码时,插入了一些上面提到过的内存屏障,来禁止处理器的重新排序。
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
ps:concurrent包的实现,利用了volatile操作的内存语意。由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:
- A线程写volatile变量,随后B线程读这个volatile变量。
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。
Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器中实现同步的关键(从本质上来说,能够支持原子性读-改-写指令的计算机器,是顺序计算图灵机的异步等价机器,因此任何现代的多处理器都会去支持某种能对内存执行原子性读-改-写操作的原子指令)。同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:
- 首先,声明共享变量为volatile;
- 然后,使用CAS的原子条件更新来实现线程之间的同步;
- 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。
AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:
- 在构造函数内对一个final变量的写入操作,与随后构造函数外把这个构造对象赋值给一个引用变量的操作,这两个操作不能被重排序。
- 编译器禁止重排
- 写final和return间加入StoreStore内存屏障
- 初次读一个包含final域的对象的引用,与初次读这个final域,这两个操作不能被重排序。
- 编辑器在读final域操作前加入LoadLoad内存屏障
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。