实现原理
//instance是volatile变量
instance = new Singleton();
转换为汇编代码:
0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock add1 $0×0,(%esp)
在有volatile变量修饰的共享变量进行写操作时会多出第二行代码。 lock前缀的指令在多核处理器中会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存。 lock指令导致在执行指令期间,声言处理器的LOCK#信号。LOCK#信号使CPU在执行期间锁住总线,其他CPU无法访问主线也就不能访问系统内存。随着发展,处理器不锁总线而变成锁缓存,毕竟锁总线开销太大。缓存锁定后,会阻止同时修改两个以上处理器缓存的内存区域数据。
- 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。 处理器能嗅探其他处理器访问系统内存和它们的内部缓存,如果一个处理器检测到其他处理器打算写内存地址,而这个地址处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,下次访问时,强制执行缓存填充,从主内存读取数据。
内存语义
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
特性
- 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
复合操作的非原子性
- 复合操作比如:
- n ++;
- n = n + 1;
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;//复合操作
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
如果inc具有原子性,那么最后得到的值应该是10000,但是事实上是小于10000的。 假如有一种情况:
inc主内存中为10,在线程1的inc ++
操作中,先读取inc的原始值,此时线程1被阻塞了,然后线程2对变量进行了inc ++
操作,而在此时线程1还没有进行写操作,那么此时inc就依旧是读取主内存的操作,所以依旧是10(本来应该是11)。