本文参考 《深入理解JAVA虚拟机》(周志明) 第二版,p362 ,对文中内容提炼加以总结而成。
- java内存模型的产生,是为了解决各个硬件和操作系统对内存访问的差异。
- 主要针对变量来定义各种规则,线程私有变量不算,因为不存在线程安全问题
- “二八原则”,二指两个内存,线程工作内存、 主内存。线程工作内存无法直接访问(读写)主内存,每个线程的线程工作内存也无法直接互相访问。这两个内存的交互有具体协议,即八个原子操作
-
lock: 作用于主内存,把一个变量表示为一条线程独占
unlock: 作用于主内存,把一个处于锁定的变量释放,释放后才可以被其他线程锁定
read:作用于主内存的变量,把主内存变量值传入工作内存,方便后面 load
load:作用于工作内存,将read读取的值载入工作内存副本
use:作用于工作哦内存,把工作内存的某个值传给执行引擎,虚拟机每次使用变量值则调用这个
assign:作用于工作内存的变量,把一个从执行引擎接收到的值复制给工作内存的变量,虚拟机每次给变量复制使用这个
store:作用于工作内存的变量,把工作内存值传给主内存 (类比read)
write:作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存中(类比load)
这8个原子操作的一些规定 和 后面 volatile的某些特殊约定,共同决定了java线程安全体系。
1. (read load) (store write) 这两组操作必须按顺序执行
2. (read load) (store write)这两组操作必须完整,不能只出现read 没有load 类似这种
3. 如果执行了 assign, 则后续必须有 store write
4. 不允许无原因的(没发生过 assign),就发生 store write
5. 新变量只能在主内存中诞生
6. lock可以被一个线程执行多次,但unlock需要相同的次数
7. lock会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign
8. 没有lock 不可 unlock
9. unlock之前 ,必须对该变量 store write
synchronized 隐式的使用lock和unlock,对于7和9来说,可以保证线程间变量的可见性
可见性:一个线程修改了这个变量的值,其他线程可以立即知道
volatile规则:
volatile能保证变量在线程之间的可见性
volatile修饰的变量具有的可见性,用8个原子操作解释为: use之前,必须 read+load,assign之后,必须 store+write
volatile能禁止指令重排序优化
重排序优化是机器级的优化操作,用代码形象解释如下:
volatile boolean initialized = false;
//thread A
loadConfig();
initialized = true;
//thread B
while(!initialized){
sleep();
}
doNext();
这代码意思就是 线程A先 loadConfig(),然后 initialized 为 true,线程B再去进行下面工作。如果进行重排序优化,则可能先在A线程 先执行 initiallized = true,后执行loadConfig(),导致B在 A线程 loadConfig() 之前跳出 while循环,显然不对。
volatile如何禁止重排序的呢,使用内存屏障
。其实重排序优化是有条件的,必须前后无依赖,比如 :
A = A+10
A *= 2
类似这里(举个栗子,不代表任意代码) 两个语句前后依赖,就不能重排序。所谓的内存屏障
,就是刻意制造这种依赖,致使代码不能重排序。(具体操作 lock addl $0x0,(%esp),把ESP寄存器的值加0,显然是一个空操作,相当于做了一次 store + write,只不过啥也没改,但是你不能越过这个操作去执行下面的操作
)
从8个原子操作理解的话,即为:
T表示线程,所以这里在同一个线程内
A B 表示操作
V W 表示volatile变量
A1:T->V use | assign
A2:T->V load | store
A3:T->V read | write
B1:T->W use | assign
B2:T->W load | store
B3:T->W read | write
A1先于B1 => A3先于B3
========
个人理解:
V = 1; (执行了 A1,A2,A3)
W = 2;(执行了 B1,B2,B3)
从代码上来看,A3 先于 B3,由于不能重排序 所以 A1 一定先于 B1