本节内容总结自《Java 并发编程的艺术》
volatile的应用
- volatile的定义
- 如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的,volatile是轻量级的synchronized,他在多处理器开发中保证了共享变量的可见性,可见性的意思是当一个线程修改一个共享变量时,其他线程能读到这个修改后的值,volatile不会引起线程上下文的切换和调度。
- volatile的实现原理
- volatile变量修饰的共享变量进行写操作时会多出一行含有Lock前缀指令的汇编指令,这个指令会引发下面两件事
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回到内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
- volatile变量修饰的共享变量进行写操作时会多出一行含有Lock前缀指令的汇编指令,这个指令会引发下面两件事
- 缓存一致性协议(MESI)
- 当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
- 嗅探技术
- 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存设置为无效,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读取到处理器缓存中。
- 嗅探的缺点
- 总线风暴:由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS操作不断循环,无效交互会导致总线带宽达到峰值。所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
- volatile的实现原则
- Lock前缀指令引起处理器缓存写回到内存。Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存,但是在最近的处理器中,LOCK#信号一般不锁总线,而是锁定缓存。也就是说如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,他会锁定这块内存区域的缓存并写回到内存,并使用缓存一致性机制来确保修改的原子性,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。处理器使用嗅探技术保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致。例如,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使他的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。
- volatile的使用优化
- volaile将共享变量追加到64字节,由于大多数处理器的高速缓存行是64个字节,不支持部分填充缓存行,这意味着,如果队列的头结点和尾节点都不足64字节的话,处理器会将他们读到同一个高速缓存行中,当一个处理器试图修改头结点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存的尾节点,而队列的出队入队操作则需要不停的修改头节点和尾节点,所以在多处理器情况下将会严重影响到队列的入队出队效率。使用追加到64字节的方式来填充到64字节的方式来填满高速缓冲区的缓存行,避免头结点和尾节点加载到同一个缓存行,使头,尾节点在修改时不会互相锁定。
- 以下两种情况volatile不会填充变量
- 处理器缓存行非64字节宽。
- 共享变量不会被频繁的写。
volatile的内存语义
volatile的特性
- 可见性:对一个volatile变量的读,总是能看到任意线程对这个volatile变量之前的写入。
- 原子性:对任意单个volatile变量的读/写操作具有原子性,但类似于volatile ++ 这种复合操作不具有原子性。
volatile写读的内存语义
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
volatile内存语义的实现
- volatile重排序规则
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile操作之前。
- 当第二个操作是写时,不管第一个操作是什么,都不能重排序。这个规则确保了volatile写之前的操作不会被编译器重排序到volatile写之后。
- 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
- 实现方式
为了实现volatile的内存语义,编译器在生成字节码的时候,会在指令中插入内存屏障来禁止特定类型的处理器重排序。编译器可以根据具体情况省略不必要的屏障。
- 在每个volatile写操作前面插入一个StoreStore屏障。来禁止上面的普通写和下面的volatile写重排序。
- 在每个volatile写操作之后插入一个StoreLoad屏障。来防止上面的Volatile写与下面有可能出现的volatile读/写重排序。
- 在每个volatile读操作之后插入一个LoadLoad屏障。禁止下面所有普通读操作和上面的volatile读重排序。
- 在每个volatile读操作之后插入一个LoadStore屏障。来禁止下面所有的普通写操作和上面的volatile读操作重排序。