定义:
java语言规范第三版:
java编程计语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获取这个变量。
java提供了volatile关键字,在某些情况下比锁更加方便。如果一个字段(变量)被volatile修饰,java线程内存模型确保所有线程看到这个变量值是一样的。
在x86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时,cpu会做什么事情。
java代码:
instance =new Singleton();//instance是volatile变量
对应的汇编代码:
0x01a3de1d:movb $x0,0x1104800(%esi);
0x0xa3de24: lock add1 $0x0,(%esp);
Lock前缀的指令在多核处理器下会引发两件事:
- 将当前处理器缓存行的数据写回到系统内存。(注:缓存行是指cpu高速缓存中可以分配的最小存储单元。处理器填写缓存 行时会加载整个缓存行)
- 这个写回内存的操作会使在其他cpu里缓存了该内存地址的数据无效(如果其他处理器对这个数据进行修改操作时,会重新从系统内存把数据读取到处理器缓存里)。
实现原理:
- Lock前缀指令会处理器缓存回写到内存
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效((可见性))
volatile的使用优化:
jdk1.7并发包新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,通过追加字节的方式来优化队列出队和入队的性能。
为什么追加字节(64)可以优化性能?
对于英特尔酷睿i7、酷睿、Atom和NetVurs等处理器的L1,L2或L3缓存高速缓存行是64个字节宽,不支持部分填充缓存行,而一个引用变量占4个字节,就有可能导致多个应用变量一起填充到同一个缓存行中,对缓存行加锁,这样会使得队列的头结点和尾节点可能出现在同一个缓存行中,而队列的入队与出队操作时在不断的修改头结点和尾节点,就可能导致头结点和为节点同时锁定,多处理器下严重影响性能。
以下情况:使用volatile变量是不应该追加到64字节:
- 缓存行宽度不是64字节(如P6系列和奔腾处理器,L1,L2是32字节宽)
- 共享变量不会被频繁的写,不频繁写,那么缓存行被锁定的几率很小
灵魂拷问:为什么需要使用volatile??
多线程安全需要考虑原子性,可见性,有序性
而volatile提供了可见性,有序性(不提供原子性,原子性可以使用atomic包,或者synchronized 关键字加锁 )
可见性: A线程修改了volatile 变量,会导致B线程持有得到该变量失效,需要重新到主内存读取,也就是A修改B线程可见
(所有线程对变量的操作都是 现将 数据从主内存读取副本到工作内存(线程私有内存),操作完成之后,刷回主内存),volatile 则可以通知 其他线程 变量失效,需要重新到 主内存读取操作
有序性:volatile 满足happensBefore 原则,如果操作次序可以由原则推出,则不会发生指令重排(通过插入内存屏障,禁用指令与内存屏障前后发生重排;内存屏障的另一个作用是通知cpu 强制刷新缓存,获取最新的值)
可见性测试代码:
class Data{
volatile int number=0;
public void changeNumber(){
number=1;
}
}
public class UnderstandVolatile {
// 测试可见性
public static void main(String[] args) {
Data data=new Data();
// 1. data 不加volatile //
// 结果:CHANGE come in
// CHANGE updated value:1
// 2. data 加上volatile
// 结果: CHANGE come in
// CHANGE updated value:1
// main: finished
// 第一个线程去改数据,用3s 完成数据更改
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" come in");
try {
Thread.sleep(3*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.changeNumber();
System.out.println(Thread.currentThread().getName()+" updated value:"+data.number);
},"CHANGE").start();
// 第二个线程 main ,等待线程 1 完成数据更改
while (data.number==0){
}
System.out.println("main: finished");
}
}
在什么地方使用volatile?
DCL=Double check Lock( 双端检测机制,单例模式会用到)