一、Java内存模型的基础
1、Java并发编程是利用共享内存的方式进行线程之间的隐式通信,在这种模型里面,同步是显式的,程序必须指定某个方式或代码块在线程间的执行是以互斥的方式进行的。
2、Java内存模型的抽象结构
Java线程之间的通讯是通过JMM模型来控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。JMM定义了主存和线程之间的抽象关系:线程之间的共享变量存储在主存中,每个线程都有自己的本地内存拷贝,本地内存存储了该线程读/写共享变量的副本,本地内存是一个抽象概念,实际上是不存在的。它包含了缓存,写缓冲区,寄存器及其它的硬件和编译器优化。
3、重排序
执行程序时,为了优化性能,一般都会进行重排序。重排序分为以下3个类型:
1).编译器优化的重排序,在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
2).指令级并行的重排序,现代处理器采用指令并行技术来进行多条指令重叠执行。如果不存在 数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
3).内存系统的重排序,由于处理器使用缓存和读/写缓冲区,使得加载和存储操作看起来是乱序执行。
Java代码从编译到执行,会经历以下的重排序:
JMM的编译器重排序规则会禁止特定类型的编译器重排序,对于处理器,JMM的处理器重排序规则会要求编译器在生成指令时,插入特定的内存屏障指令(Memory Barriers)来禁止重排序。
3.1.数据依赖性
如果两个操作访问一个共享变量,且其中一个操作有写操作,这两个操作就存在依赖关系。在单一线程中的,编译器和处理器不会改变两个操作的执行顺序,但在多线程和多处理器时,数据依赖性不会被考虑。
3.2.as-if-serial
As-if-serial指不管理怎么排序,在单线程中程序的执行结果不能改变。编译器、Runtime和处理器都必须遵守。
3.3.程序规则顺序
JMM仅要求前一操作的执行结果必须对后一操作可见。
4、顺序一致性模型
JMM对正确同步的多线程的内存一致性做了如下保证:
如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)。即程序的执行结果和该程序在顺序一致性内存模型里面的执行结果相同。
5、Volatile的内存语义
当写一个Volatile变量时,JMM会把线程对应的本地内存中的共享变量刷到主存。
当读一个Volatile变量时,JMM会把该线程对应的本地内存置为无效,再从主存中读取共享变量 ;
总结:
• 线程A写一个volatile变量 ,实质上是线程A向接下来要读这个volatile变量的线程发出了(其对共享变量所作修改的)消息。
• 线程B读取一个volatile变量 ,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做的个性)消息;
• 线程A写一个volatile变量,随即线程B读取这个变量,实质上是线程A通过主存向线程B发送消息
5.2.volatile内存语义的实现
为了实现volatile的内存语义,JMM会限制编译器重排序和处理器重排序。
JMM针对编译器的重排序限制如下表:
编译器会在生成字节码时,在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
基于保守策略的JMM内存屏障:
• 在每个volatile写操作前插入一个StoreStore屏障;
• 在每个volatile写操作后插入一个StoreLoad屏障;
• 在每个volatile读操作后插入一个LoadLoad屏障;
• 在每个volatile读操作后插入一个LoadStore屏障;
写的示意图:
StoreStore屏障可以保障在volatile写之前的所有普通写操作对处理器可见,因为StoreStore屏障把普通写刷到主存中。
读的示意图:
6、锁的内存语义
当线程释放锁时,JMM会把线程的本地内存的共享变量刷到主存中。
当线程获取锁时,JMM会把线程的本地内存的共享变量置无效。
总结:
• 线程A释放一个锁,实际上是线程A向要获取锁的线程发出了(线程A对共享变量做了修改的)消息 ;
• 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
• 线程A释放锁,随后线程B获取锁,这个过程实质上是线程通过主存与线程B通信。
6.1.锁内存语义的实现
从ReentrantLock类的公平锁和非公平锁的实现来看,锁的内存语义实现可以有以下2种方式:
• 利用volatile变量读写的内存语义;
• 利用CAS所附带的volatile读写的内存语义;
7、final的内存语义
对于final域,编译器和处理器要遵守两个重排序规则:
• 在构造函数里面对一个final域的写入,与随后把这个被构造的对象的引赋值给一个引用变量,这两个操作之间不能重排序;
• 初次读一个包含finail域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
7.1.写final域的重排序规则
写final域的重排序规则禁止把final域的写重排序到构造函数之外,这个规则包含两个方面:
• JMM禁止编译器把final域的写重排序到构造函数之外;
• 编译器会在final域写之后,构造函数return之前,插入一个StoreStore屏障;这个屏障禁止处理器把final域的写重排序到构造函数之外。
7.2.读final域的重排序规则
在一个线程中,初次读对象引用与初次读对象引用包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作之前插入一个LoadLoad屏障。
7.3.final域为引用类型
对于引用类型,写final域的重排序规则对编译器和处理器添加了以下约束:在构造函数内对一个final引用的对象的域成员的写入,与随后在构造函数外把这个被构造的对象的引用赋于一个引用变量,这两个操作之间不能重排序。
7.4.final域所在的对象引用不能从构造函数中“逸出”
this可能导致从构造函数逸出,下面是例子:
1 | public class FinalReferenceEscapeExample { |