valatile原理

一 volatile定义

官方定义:

java语言规范第三版中对volatile的定义如下:java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排它锁单独获得这个变量。java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明或volatile,java线程内存模型确保所有线程看到这个变量值是一致的。

个人定义:

volatile的解释通常是“易变的,不稳定的”。这也正是使用volatile关键字的语义。当你用volatile声明一个变量时,就等于告诉了虚拟机,这个变量极有可能被某些程序或者线程修改。为了确保这个变量被修改后,应用程序范围内所有线程都能够“看到”这个改动,虚拟机就必须采用某些特殊手段,保证这个变量可见性等特点。

二 volatile有两个性质,内存可见性和禁止指令重排序

内存可见性:

内存可见性表现在只要用该关键字修饰的域,当这个域内数据发生变化时,所有读操作都可以看到这个修改,哪怕是使用了本地缓存,volatile域也会写入到主存中,而读写操作正是发生在主存中。(主存又叫主存储器 Main memory ,是计算机硬件的一个重要部件,其作用是存放指令和数据,并能由中央处理器(CPU)直接随机存取)

指令重排序

在JDK中,JAVA语言为了维持顺序内部的顺序化语义,也就是为了保证程序的最终运行结果需要和在单线程严格意义的顺序化环境下执行的结果一致,程序指令的执行顺序有可能和代码的顺序不一致,这个过程就称之为指令的重排序。指令重排序的意义在于:JVM能根据处理器的特性,充分利用多级缓存,多核等进行适当的指令重排序,使程序在保证业务运行的同时,充分利用CPU的执行特点,最大的发挥机器的性能!

三 内存屏障与volatile的关系

内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令:
a) 确保一些特定操作执行的顺序;
b) 影响一些数据的可见性(可能是某些指令执行后的结果)。
编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制 更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

四 volatile的实现原理

  1. 那么Volatile是如何来保证可见性的呢?在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。

Java代码: instance = new Singleton();//instance是volatile变量
汇编代码: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);

将当前处理器缓存行的数据会写回到系统内存。有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。

  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
扩展一下:L1,L2为cpu的一级缓存和二级缓存,相应的L3为三级缓存,它们的容量逐渐递增,成本逐渐递减。L2相当于L1的缓冲器,存储L1需要用到而又无法存储的数据。L3为L2的缓冲器。需要注意,L2和L3和内存都不能存储原始指令,只能存储临时数据。指令只能存在cpu的一级缓存中。

五 Happen-Before规则

虽然Java虚拟机和执行系统会对指令进行一定的重排,但是指令重排是有原则的,并非所有指令都可以随随便便改变执行位置,以下罗列了一些基本规则,这些原则是指令重排不可违背的。

  • 程序顺序原则:一个线程内保证语义的串行性
  • volatile原则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start()方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行、结束先于finalize()方法

六 volatile的应用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。
通常来说,使用volatile必须具备以下2个条件:

  • 对变量的写操作不依赖于当前值
  • 该变量没有包含在具有其他变量的不变式中
    实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
    事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。
    下面列举个Java中使用volatile的几个场景:
class Singleton{
	private volatile static Singleton instance = null;
	private Singleton() {
	      
	}
	  
	public static Singleton getInstance() {
	    if(instance==null) {
	        synchronized (Singleton.class) {
	            if(instance==null)
	                instance = new Singleton();
	        }
	    }
	    return instance;
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章