可见性: 关键就是保证load、use的执行顺序不被打乱(保证使用变量前一定进行了load操作,从主存拿最新值来),assign、wirte的执行顺序不被打乱(保证赋值后马上就是把值写到主存),所以使用内存屏障。
有序性: Java 内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。
volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
volatile 特性
可以把对volatile 变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步
可以看成
可见性: 对一个volatile 变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
原子性: 对任意单个volatile 变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile 的内存语义
内存语义:可以简单理解为volatile,synchronize,atomic,lock 之类的在JVM中的内存方面实现原则。
volatile 写的内存语义如下: 当写一个volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile 读的内存语义如下: 当读一个volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
为何volatile 不是线程安全的
volatile 内存语义的实现
volatile 重排序规则表
当第二个操作是volatile 写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile 写之前的操作不会被编译器重排序到volatile 写之后。
当第一个操作是volatile 读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile 读之后的操作不会被编译器重排序到volatile 读之前。
当第一个操作是volatile 写,第二个操作是volatile 读时,不能重排序。
volatile 的内存屏障
在Java 中对于volatile 修饰的变量,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序问题。
storestore 屏障:对于这样的语句store1; storestore; store2,在store2 及后续写入操作执行前,保证store1 的写入操作对其它处理器可见。(也就是说如果出现storestore 屏障,那么store1 指令一定会在store2 之前执行,CPU 不会store1与store2 进行重排序)
storeload 屏障:对于这样的语句store1; storeload; load2,在load2 及后续所有读取操作执行前,保证store1 的写入对所有处理器可见。(也就是说如果出现storeload 屏障,那么store1 指令一定会在load2 之前执行,CPU 不会对store1 与load2 进行重排序)
在每个volatile 读操作的后面插入一个LoadLoad 屏障。在每个volatile 读操作的后面插入一个loadstore 屏障。
loadload 屏障:对于这样的语句load1; loadload; load2,在load2 及后续读取操作要读取的数据被访问前,保证load1 要读取的数据被读取完毕。(也就是说,如果出现loadload 屏障,那么load1 指令一定会在load2 之前执行,CPU不会对load1 与load2 进行重排序)
loadstore 屏障:对于这样的语句load1; loadstore; store2,在store2 及后续写入操作被刷出前,保证load1 要读取的数据被读取完毕。(也就是说,如果出现loadstore 屏障,那么load1 指令一定会在store2 之前执行,CPU 不会对load1 与store2 进行重排序)
volatile 的实现原理
通过对OpenJDK 中的unsafe.cpp 源码的分析,会发现被volatile 关键字修饰的变量会存在一个“lock:”的前缀。
Lock 前缀,Lock 不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU 总线和高速缓存加锁,可以理解为CPU 指令级的一种锁。
同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU 里缓存了该地址的数据无效。
在具体的执行上,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。在Lock 锁住总线的时候,其他CPU 的读写请求都会被阻塞,直到锁释放。