java线程之可见性
volatile不需要加锁, 比synchronized更轻量级, 不会阻塞线程; 从内存可见性角度看, volatile读相当于加锁, volatile写相当于解锁。
synchronized既能保证可见性, 又能保证原子性; volatile只能保证可见性,无法保证原子性。
同步
退出同步块 -> 释放监视器 -> 刷新缓冲区到主内存; 进入同步块 -> 获取监视器 -> 本地处理器缓存失效 -> 重新从主内存加载变量。
线程A与线程B之间如要通信: 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
happens-before
从JDK5开始,java使用新的JSR -133内存模型。JSR-133提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性:
- 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
- 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
- volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
- 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。
重排序
重排序分为三种: 编译器优化的重排序, 指令级并行的重排序, 内存系统的重排序。
- 对于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序。
- 对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
内存屏障的四种分类:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barriers | Load1; LoadLoad; Load2 | 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。 |
StoreStore Barriers | Store1; StoreStore; Store2 | 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。 |
LoadStore Barriers | Load1; LoadStore; Store2 | 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。 |
StoreLoad Barriers | Store1; StoreLoad; Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。 |
数据依赖关系(分为三种: 写后读, 写后写, 读后写)
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。
程序顺序规则:如果A happens- before B,JMM并不要求A一定要在B之前执行。JMM仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。如果重排序操作A和操作B后的执行结果与按happens- before顺序执行的结果一致。在这种情况下,JMM会认为这种重排序并不非法,JMM允许这种重排序。
顺序一致性
顺讯一致性内存模型是一个被计算机科学家理想化了的理论参考模型。其有两大特性:
- 一个线程中的所有操作必须按照程序的顺序来执行。
- (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
同步程序的顺序一致性效果:
- 在顺序一致性模型中,所有操作完全按程序的顺序串行执行。而在JMM中,临界区内的代码可以重排序。JMM会在退出监视器和进入监视器这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程A在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程B根本无法“观察”到线程A在临界区内的重排序。
JMM不保证未同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。未同步程序在JMM和顺序一致性模型的执行特性有以下差异:
- 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而JMM不保证单线程内的操作会按程序的顺序执行(比如同步的多线程程序在临界区内的重排序)。
- 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而JMM不保证所有线程能看到一致的操作执行顺序。
- JMM不保证对64位的long型和double型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。
volatile
volatile变量特性:
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile写-读的内存语义:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
小结: 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
为了实现volatile内存语义,JMM会分别限制编译器重排序和处理器重排序, JMM针对编译器制定的volatile重排序规则如下:
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
对于x86处理器,仅会对写-读操作做重排序。X86不会对读-读,读-写和写-写操作做重排序,因此在x86处理器中会省略掉这三种操作类型对应的内存屏障。在x86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义 。
锁
锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。
公平锁内存语义的实现:
- 公平锁在释放锁的最后写volatile变量state;在获取锁时首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取同一个volatile变量后将立即变的对获取锁的线程可见。
公平锁和非公平锁的区别:
- 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
- 公平锁获取时,首先会去读这个volatile变量。非公平锁获取时,首先会用CAS[即java的compareAndSet()方法调用]更新这个volatile变量,这个操作同时具有volatile读和volatile写的内存语义。