volatile:线程访问共享变量,通过排它锁单独获得该变量
volatile=轻量级synchronized:不会引起线程上下文切换调度,使用执行成本低
原理
操作系统知识
volatile修饰的变量写操作时会在前面加LOCK前缀≈内存屏障
- 将当前缓存行的数据写回内存
LOCK锁缓存不锁总线(锁总线,阻塞其他cpu访问内存,开销大)
缓存锁定:如果访问的内存区域已经缓存在处理器内部,则锁定该内存的缓存,并写回到内存,使用缓存一致性机制保证修改原子性
缓存一致性机制:阻止同时修改由两个以上处理器缓存的内存数据区域 - 其他cpu(多处理器)缓存了该内存地址的数据无效
处理器嗅探总线判断数据是否有效(其他处理器打算写内存地址&该内存地址处于共享状态,则无效),无效则设为无效重新缓存
∴volatile直接存于主存,易变,不稳定
内存屏障:为了性能,编译器和处理器会对指令进行重排序,内存屏障是一条指令,对指令重排做出一定限制。指令重排只能保证在单线程执行下逻辑正确,多线程可能会出错
特性
- 可见性:变量在线程间可见性,基于cpu内存屏障指令
boolean flag = true;
while(flag){
xxx
}
flag = false;
/*
JMM:主存共享,线程栈(工作内存)线程私有
线程1:修改flag(线程栈中),还未写回主存,转去做其他事
线程2:不知道,一直循环
*/
-
顺序性:禁止指令重排序,效率↓
volatile前的语句执行完且对后可见
volatile后的语句不能重排到volatile前- 编译期:JMM遵循内存屏障约束
StoreStore LoadLoad volatile变量写 volatile变量读 StoreLoad LoadStore - 运行期:依靠cpu屏障指令
-
原子性:与CAS结合保证原子性,volatile本身不保证操作原子性。
只能单独的读或者写,读+写 之间是可以被中断的,这意味着在读取或者修改 volatile 变量的过程中,其他线程可能会对这个变量进行修改。 -
线程安全:volatile不能保证操作的原子性,所以并不是线程安全的 eg:i++;
当满足以下条件时volatile是线程安全的
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
内存语义
- 写volatile变量,JMM将该线程对应的本地内存中的共享变量刷新到主内存
- 读volatile变量,JMM将该线程对应的本地内存置为无效,从主内存中读取共享变量
总结:
- 写volatile变量,实质上是告诉其他读该变量的线程,要修改共享变量
- 读volatile变量,实质上是接收了其他线程修改共享变量的消息
- 线程a写,线程b读,实质上是a通过主内存向b发送消息
实现:
第二个操作是volatile写时,第一个操作不管是什么,都不能重排序
第一个操作是volatile读时,第二个操作不管是什么,都不能重排序
第一个操作是volatile是写,第二个操作是volatile是读,不能重排序
Load:加载(读)、Store:保存(写)
JMM内存屏障插入策略:
在每个volatile写操作前插入StroreStore屏障
在每个volatile写操作前插入StroreLoad屏障
在每个volatile读操作前插入LoadLoad屏障
在每个volatile读操作前插入LoadStore屏障
优化
一般高速缓存行64字节宽(有的32),不支持部分填充
共享变量不足64字节,则与其他数据共同存在于一个高速缓存行。
多cpu,每个处理器都会缓存同样的数据
共享变量频繁写,则频繁锁定当前缓存行,其他处理器数据失效。
∴补足64字节(高速缓存行的长度)独占一个高速缓存行。锁定时不影响其他数据
注:java7可能不生效,会淘汰/重新排序无用字段
volatile与synchronized
volatile | synchronized | |
---|---|---|
修饰 | 变量 | 方法、代码块、类对象 |
性能 | 线程同步的轻量级实现。volatile=轻量级synchronized:不会引起线程上下文切换调度,使用执行成本低 | |
多线程 | 不阻塞 | 阻塞 |
三大特性 | 保证可见、有序,不保证原子 | 保证可见、有序、原子 |
同步 | 变量在主存和工作内存间同步 | 多线程访问资源同步 |
优化 | 编译器优化 |
volatile和CAS底层实现都用CPU的lock指令,有什么不同?
- lock是前缀,后面跟命令,具体看后面的命令
- volatile不保证原子性,实现需要内存屏障,lock前缀的指令具有内存屏障的效果,作内存屏障使用的。
- CAS保证原子性,CAS的实现用了lock cmpxchg指令。cmpxchg指令涉及一次内存读和一次内存写,需要lock前缀保证中间不会有其它cpu写这段内存。
lock:执行时,会设置处理器的LOCK#信号(这个信号会锁定缓存,阻止其它CPU修改,直到这些指令执行结束),这条指令的执行变成原子操作,之前的读写请求都不能越过lock指令进行重排,相当于一个内存屏障。