一个被volatile修饰的共享变量具备两层语义:
1、保证可见性,即某线程修改了该变量的值,则新值对其他线程来说是立即可见的。不保证原子性。
正常情况下:为了提高处理速度,每个线程都有自己的缓存,同时在各自缓存中都有一份共享变量的副本,各线程对共享变量的修改也仅仅是针对自身缓存的副本。那么当某线程对共享变量进行更改时,其他线程并不知情。
当使用volatile修饰:
- 修改后的变量值会被立即写入主存。
- 某个线程对共享变量进行的更改,会导致其他线程缓存中的该共享变量无效。当其他线程再次读取共享变量时发现自己的缓存行无效,则会重新把数据从主存加载到缓存。
之所以说不保证原子性:
如i++,它包括3个原子操作:读取变量的原始值、进行加1操作、写入缓存。若多个线程在执行第一个原子操作后被阻塞,则当某个线程执行i++后,这些线程并不知情,从而导致i只加1。
2、禁止进行指令重排序。
正常情况下,为了在不改变单线程下程序执行结果的前提下,优化程序的运行效率,编译器或CPU会自动对指令进行优化,从而导致各指令之间执行的先后顺序发生变化。
volatile禁止指令重排序有两层意思:
volatile禁止指令重排序有两层意思:
- 在进行指令优化时,不能将“对volatile变量访问的语句”调整到该语句前面的命令之前,也不能把“对volatile变量访问的语句”调整到该语句后面的命令之后。
- 当程序执行到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;
}
}
其中语句“instance = new Singleton()”并非是一个原子操作,它包括3个操作:
(1)给instance分配内存
(2)调用Singleton的构造函数来初始化成员变量
(3)将instance对象指向分配的内存空间。
指令重排时,2和3的执行顺序是不能保证的(1-2-3或 1-3-2)。若为后者,则当某线程在3执行完毕、2未执行之前被其他线程抢占,则其他线程会返回一个未初始化的instance。
volatile实现原理
1、可见性。
- 当对volatile变量进行写操作时,编译器会向处理器发送一条Lock前缀的指令。该命令会强制修改后的值写回到主存,从而确保了若某线程对volatile变量进行修改,则会立即更新主存中数据。
- 在多处理器环境下,各处理器会检查自己的缓存是否过期。当处理器发现自己缓存行对应的内存地址被修改了,就会将该缓存行设置成无效状态;当处理器要对该volatile变量进行读写操作时,会强制重新把数据从系统内存加载到处理器缓存中,从而确保了其他线程获取的volatile变量都是从主存中获取最新的。
2、有序性。
- Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。