volatile
volatile [ˈvɑːlətl] 挥发性的;不稳定的;爆炸性的;反复无常的 synchronized [ˈsɪŋkrənaɪzd] 同步的;同步化的
1 volatile
的特性
Java
支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。
关键字volatile
可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
举个例子,定义一个表示程序是否运行的成员变量boolean on = true
,那么另一个线程可能对它执行关闭动作on = false
,这里涉及多个线程对变量的访问,因此需要将其定义成为volatile boolean on = true
,这样其他线程对它进行改变时,可以让所有线程感知到变化,因为所有对on
变量的访问和修改都需要以共享内存为准。但是,过多地使用volatile
是不必要的,因为它会降低程序执行的效率。
如果对声明了volatile
的变量进行写操作,JVM
就会向处理器发送一条Lock
前缀的指令,将这个变量所在缓存行的数据写回到系统内存。 但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操就会有问题。
所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一 致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
volatile
的两条实现原则:理器缓存回写到内存;一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
volatile
在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile
变量修饰符使用恰当的话,它比synchronized
的使用和执行成本更低,因为它不会引起线程上下文的切换和调度,可以说,volatile
是轻量级的synchronized
。
一个volatile
变量的单个读/写操作,与一个普通变量的读/写操作都是使用同一个锁来同步,它们之间的执行效果相同。 代码如下:
class VolatileFeaturesExample {
volatile long vl = 0L; // 使用volatile声明64位的long型变量
public void set(long l) {
vl = l; // 单个volatile变量的写
}
public void getAndIncrement () {
vl++; // 复合(多个)volatile变量的读/写
}
public long get() {
return vl; // 单个volatile变量的读
}
}
在语义上等价于:
class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通变量
public synchronized void set(long l) {
vl = l; // 对单个的普通变量的写用同一个锁同步
}
public void getAndIncrement () {
long temp = get(); // 普通方法调用
temp += 1L; // 调用已同步的读方法
set(temp); // 普通写操作
} // 调用已同步的写方法
public synchronized long get() { // 对单个的普通变量的读用同一个锁同步
return vl;
}
}
锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64
位的long
型和double
型变量,只要它是volatile
变量,对该变量的读/写就具有原子性。 如果是多个volatile
操作或类似于volatile++
这种复合操作,这些操作整体上不具有原子性。
volatile
变量自身具有下列特性:
- 可见性:对一个
volatile
变量的读,总是能看到(任意线程)对这个volatile
变量最后的写入。 - 原子性:对任意单个
volatile
变量的读/写具有原子性,但类似于volatile++
这种复合操作不具有原子性。
2 volatile
写-读的内存语义
从JDK 1.5
开始,volatile
变量的写-读可以实现线程之间的通信。从内存语义的角度来说,volatile
的写-读与锁的释放-获取有相同的内存效果,volatile
写和锁的释放有相同的内存语义;volatile
读与锁的获取有相同的内存语义。
当写一个volatile
变量时,JMM
会把该线程对应的本地内存中的共享变量值刷新到主内存。
假设线程A
首先执行writer()
方法,随后线程B
执行reader()
方法,初始时两个线程的本地内存中的flag
和a
都是初始状态。下图是线程A
执行volatile
写后,共享变量的状态示意图:
如图所示,线程A
在写flag
变量后,本地内存A
中被线程A
更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A
和主内存中的共享变量的值是一致的。
当读一个volatile
变量时,JMM
会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
下图为线程B
读同一个volatile
变量后,共享变量的状态示意图:
如图所示,在读flag
变量后,本地内存B
包含的值已经被置为无效。此时,线程B
必须从主内存中读取共享变量。线程B
的读取操作将导致本地内存B
与主内存中的共享变量的值变成一致。
如果把volatile
写和volatile
读两个步骤综合起来看的话,在读线程B
读一个volatile
变量后,写线程A
在写这个volatile
变量之前所有可见的共享变量的值都将立即变得对读线程B
可见。
volatile
写和volatile
读的内存语义做个总结:
- 线程
A
写一个volatile
变量,实质上是线程A
向接下来将要读这个volatile
变量的某个线程发出了(其对共享变量所做修改的)消息。 - 线程
B
读一个volatile
变量,实质上是线程B
接收了之前某个线程发出的(在写这个volatile
变量之前对共享变量所做修改的)消息。 - 线程
A
写一个volatile
变量,随后线程B
读这个volatile
变量,这个过程实质上是线程A
通过主内存向线程B
发送消息。
3 volatile
内存语义的实现
为了实现volatile
的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序(有序性)。
下图是volatile
重排序规则表:
从表中可以看出:
- 当第二个操作是
volatile
写时,不管第一个操作是什么,都不能重排序。 - 当第一个操作是
volatile
读时,不管第二个操作是什么,都不能重排序。 - 当第一个操作是
volatile
写,第二个操作是volatile
读时,不能重排序。
4 volatile
VS synchronized
volatile
本质是在告诉JVM
当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;volatile
仅能使用在变量级别;synchronized
则可以使用在变量、方法、和类级别的;volatile
仅能实现变量的修改可见性,不能保证原子性;而synchronized
则可以保证变量的修改可见性和原子性;volatile
不会造成线程的阻塞;synchronized
可能会造成线程的阻塞;volatile
标记的变量不会被编译器优化;synchronized
标记的变量可以被编译器优化;