简介
Synchronized能够实现原子性和可见性,在Java内存模型中,Synchronized规定线程在加锁时,先清空工作内存,从主内存中拷贝最新变量的副本到工作内存,执行完代码后再刷新到主内存中,此时才能释放锁。
volatile是轻量级的synchronized,如果使用恰当的话,它会比synchronized的使用成本更低,因为它不会引起线程上下文的切换和调度。如果一个字段被声明成volatile,它将具有以下两个特性:
- 可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止指令重排序。
volatile的实现原理
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
简单来说就是Lock前缀指令会引起处理器缓存回写到内存,而一个处理器的缓存回写到内存会导致其它处理器的缓存无效。
volatile的内存语义
volatile写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效。线程接下来将从主内存中读取共享变量。
为了实现volatile内存语义,JMM会限制重排序。其重排序规则如下:
- 当第二个操作是
volatile写时,不管第一个操作是什么,都不能重排序。 - 当第一个操作是
volatile读时,不管第二个操作是什么,都不能重排序。 - 当第一个操作是
volatile写,第二个操作是volatile读时,都不能重排序。
volatile的底层实现,是通过插入内存屏障。但是对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JMM采用了保守策略,策略如下:
- 在每个
volatile写操作的前面插入一个StoreStore屏障。 - 在每个
volatile写操作的后面插入一个StoreLoad屏障。 - 在每个
volatile读操作的后面插入一个LoadLoad屏障。 - 在每个
volatile读操作的后面插入一个LoadStore屏障。
原因如下:
StoreStore屏障:保证在volatile写之前,其前面的所有普通写操作,都已经刷新到主内存中。StoreLoad屏障:避免volatile写,与后面可能有的volatile读/写操作重排序。LoadLoad屏障:禁止处理器把上面的volatile读,与下面的普通读重排序。LoadStore屏障:禁止处理器把上面的volatile读,与下面的普通写重排序。
此策略可以保证在任意处理器平台,任意程序中都能得到正确的volatile内存语义,但其实在实际中,只要不会改变volatile的内存语义,编译器可以根据具体情况优化,省略不必要的屏障。