简介
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
的内存语义,编译器可以根据具体情况优化,省略不必要的屏障。