1. 内存模型的基础
- 编发编程模型的两个关键问题
1) 线程之间的通信
通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信有两种机制:共享内存和消息传递。
在共享内存的并发模型中,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。Java并发采用的是共享内存模型。
在消息传递的并发模型里,线程之间没有公共的状态,线程之间必须通过发送消息来显式进行通信
2) 线程之间的同步
同步指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。- Java内存模型的抽象结构
Java线程之间的通信由Java内存模型 JMM 控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见。
抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。- 从源代码到指令序列的重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
1) 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
2) 指令级并行的重排序:现代处理器采用指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
3) 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是在乱序执行
2. 重排序
- 数据依赖性
如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时两个操作之间存在数据依赖性。- as-if-serial语义
as-if-serial语义的意思是不管怎样的重排序,(单线程)程序的执行结果不能被改变。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作作重排序,因为这种重排序会改变执行结果。- 重排序对多线程的影响
在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果
3. 顺序一致性
- 顺序一致性内存模型
1) 一个线程中的所有操作必须按照程序的顺序来执行
2)(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见- 同步程序的顺序一致性
顺序一致性模型中,所有操作完全按照程序的顺序执行。而在JMM中,临界区内的代码可以重排序(JMM不允许临界区内的代码逸出到临界区外,这样会破坏监视器语义)。JMM会在退出和进入临界区做特别处理,使得这两个时间点具有与顺序一致性相同的内存视图。- 未同步程序的执行差异
1)JMM不能保证单线程内的操作会按程序的顺序执行
2)JMM不能保证所有线程能看到一致的操作执行顺序
3)JMM不能保证对64位的long和double型变量的写操作具有原子性
4. volatile的内存语义
- volatile的特性
1)可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
2)原子性:对任意单个volatile变量的读/写具有原子性,但类似i++这种复合操作不具有原子性- volatile 写-读的内存语义
volatile写内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存
volatile读内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程将从主内存中读取共享变量- volatile内存语义的实现
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。基于保守策略的JMM内存屏障插入策略:
1)在每个volatile写操作前面插入StoreStore屏障
2)在每个volatile写操作后面插入StoreLoad屏障
3)在每个volatile读操作后面插入LoadLoad屏障
4)在每个volatile读操作后面插入LoadStore屏障
5. 锁的内存语义
- 公平锁与非公平锁的内存语义
1)公平锁和非公平锁释放时,最后都要写一个volatile变量state
2)公平锁获取时,首先会去读volatile变量
3)非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和写的内存语义- CAS: 如果当前状态值等于预期状态值,则以原子方式将同步状态设置为给定的更新值。
- Java线程之间的通信4种方式
1)A线程写volatile变量,随后B线程读这个volatile变量
2)A线程写volatile变量,随后B线程CAS更新这个volatile变量
3)A线程用CAS更新这个变量,随后B线程用CAS更新这个volatile变量
4)A线程用CAS更新这个变量,随后B线程读这个volatile变量- concurrent包通用化实现模式
首先,声明共享变量为volatile
然后,使用CAS的原子条件实现线程之间的同步
同时,配合volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信
AQS,非阻塞数据结构,原子变量类这些concurrent包中基础类都是使用这种模式实现,而concurrent包中高层的类依赖于基础类来实现。
6.final域的内存语义
- final域的重排序规则
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序- 写final域的重排序规则
1)JMM禁止编译器把final域的写重排序到构造函数外
2)编译器会在final域的写之后,构造函数return之前,插入StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数外- 读final域的重排序规则
在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入LoadLoad屏障- final域为引用类型
约束:在构造函数内对一个final引用对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
7. happens-before
- JMM设计
JMM向程序员提供的happens-before 规则满足程序员的需求,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
5)start()规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作
6)join()规则:如果线程A执行ThreadB.join()并成功返回,那么线程B中的任意操作happens-before 于线程A从ThreadB.join()操作成功返回
7)程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
对象
8)finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。