目录
概述
声明共享变量为volatile后, 对这个变量的读/写将会很特别. 为了揭开volatile的神秘面纱, 下面将介绍volatile的内存语义及volatile内存语义的实现.
volatile的特性
理解volatile特性的一个好方法就是把对volatile变量的单个读/写, 看成是使用同一个锁对这些单个读/写操作做了同步.
意思就是
public class Demo { volatile long val = 10L; public void setVal(long val) { this.val = val; } public long get() { return val; } }与
public class Demo { volatile long val = 10L; public synchronized void setVal(long val) { this.val = val; } public synchronized long get() { return val; } }是一样的效果.
如上所示, 一个volatile变量的单个读/写操作, 与一个使用同一个锁来同步的普通变量的的读/写, 它们的执行效果是相同的.
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性, 这意味着对一个volatile变量的读, 总是能看到(任意线程)对这个volatile变量最后的写入.
锁的内存语义决定了临界区代码的执行具有原子性. 这就意味着, 即使是64位的long类型和double类型变量, 只要它是volatile变量, 对该变量的读/写就具有原子性. 如果多个volatile操作或类似于volatile++这种复合操作, 这些操作整体上不具有原子性.
简言之, volatile变量自身具有以下特性.
- 可见性: 对一个volatile变量的读, 总是能看到(任意线程)对这个volatile变量最后的写入.
- 原子性: 对任意单个volatile变量的读写具有原子性, 单类似于volatile++这种复合操作不具有原子性.
volatile读/写的内存语义
volatile写的内存语义
当写一个volatile变量时, JMM会把线程对应的本地内存中的共享变量值刷新到主内存中.
volatile读的内存语义
当读一个volatile变量时, JMM会把该线程对应的本地内存置为无效, 线程接下来将从主内存中读取共享变量.
总结
对volatile写和volatile读的内存语义做个总结:
- 线程A写一个volatile变量, 实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改的)消息.
- 线程B读一个volatile变量, 实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息.
- 线程A写一个volatile变量, 随后线程B读这个volatile变量, 这个过程实质上是线程A通过主内存向线程B发送消息.
volatile内存语义的实现
为了实现volatile的内存语义, JMM会限制编译器和处理器的重排序. 下面是限制的重排序规则.
- 当第二个操作为volatile写时, 不管第一个操作是什么, 都不能重排序. 确保了volatile写之前的操作不会被编译器重排序到volatile写之后.
- 当第一个操作为volatile读时, 不管第二个操作是什么, 都不能重排序. 确保了volatile读之后的操作不会被编译器重排序到volatile读之前.
- 当第一个操作为volatile写时, 第二个操作是volatile读时, 不能被重排序.
为了实现volatile的内存语义, 编译器在生成字节码时, 会在指令序列中插入内存屏障来禁止特定类型的处理器重排序. JMM采取保守策略, 基于保守策略的JMM内存屏障插入策略如下:
- 在每个volatile写操作的前面插入一个StoreStore屏障.
- 在每个volatile写操作的后面插入一个StoreLoad屏障.
- 在每个volatile读操作的后面插入一个LoadLoad屏障.
- 在每个volatile读操作的后面插入一个LoadStore屏障.
上述内存屏障插入策略非常保守, 但它可以保证在任意处理器平台, 任意的程序中都能得到正确的volatile内存语义. 这也是为Java提供的跨平台打下的基础. 当然编译器可以根据具体的情况省略不必要的屏障.
处理器的重排序规则
| 处理器\规则 | LoadLoad | LoadStore | StoreStore | StoreLoad | 数据依赖 |
|---|---|---|---|---|---|
| SPARC-TSO | N | N | N | Y | N |
| X86(X64 & AMD64) | N | N | N | Y | N |
| IA64 | Y | Y | Y | Y | N |
| PowerPC | Y | Y | Y | Y | N |
单元格中的"N"表示处理器不允许两个操作重排序, "Y"表示允许重排序.
可以发现常见的处理器都允许StoreLoad重排序; 常见的处理器都不允许对存在数据依赖的操作做重排序. SPARC-TSO和X86拥有相对较强的处理器内存模型, 它们仅允许对写-读操作做重排序(因为它们都使用了写缓冲区).
内存屏障类型表
|
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率
|
|---|
