volatile在java中很常见,比如懒汉式单例。那为什么单例模式要加volatile呢?加volatile究竟有什么用呢?现在我们深入剖析一下
volatile
关键字
Java 内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其 JVM 模型大致如下图。
JVM 模型规定:1) 线程对共享变量的所有操作必须在自己的内存中进行,不能直接从主内存中读写; 2) 不同线程之间无法直接访问其它线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成。
这样的规定可能导致的后果是:线程对共享变量的修改没有即时更新到主内存,或者线程没能够即时将共享变量的最新值同步到工作内存中,从而使得线程在使用共享变量的值时,该值并不是最新的。这就引出了内存可见性。
内存可见性(Memory Visibility)是指当某个线程正在使用对象状态,而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。
可见性错误是指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。
通过一个小程序,了解一下内存可见性
的重要性。
public class TestVolatile {
public static void main(String[] args) {
ThreadDemo td = new ThreadDemo();
new Thread(td).start();
while(true){ //读到的一直是flag = false
if(td.isFlag()){
System.out.println("------------------");
break;
}
}
}
}
class ThreadDemo implements Runnable {
private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
flag = true;
System.out.println("flag=" + isFlag());
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
//输出:
//flag=true
//注意没有输出"----------"
//即使 flag已经改成了true,但是主线程中的flag其实一直是false
//(工作内存中flag=false)
Java 提供了一种稍弱的同步机制,即 volatile 变量,用来确保将变量的更新操作通知到其它线程。当把共享变量声明为 volatile 类型后,线程对该变量修改时会将该变量的值立即刷新回主内存,同时会使其它线程中缓存的该变量无效,从而其它线程在读取该值时会从主内中重新读取该值(参考缓存一致性)。因此在读取 volatile 类型的变量时总是会返回最新写入的值。
volatile屏蔽掉了JVM中必要的代码优化(指令重排序),所以在效率上比较低。
//上面的小程序如果这样设置:
private volatile boolean flag = false;
//输出结果:
flag=true
------------------
Java语言规范JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑顺序不一致,这个过程就叫做指令的重排序。
指令重排序的意义:使指令更加符合CPU的执行特性,最大限度的发挥机器的性能,提高程序的执行效率。
简而言之,指令重排序就是CPU的优化过程(指令重排序不是一无是处的)
同样,眼见为实,乱序排序的证明:
如果没有指令重排序,不可能出现x=0&&y=0
package com.mashibing.jvm.c3_jmm;
public class T04_Disorder {
private static int x = 0, y = 0;
private static int a = 0, b =0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread one = new Thread(new Runnable() {
public void run() {
//由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
//shortWait(100000);
a = 1;
x = b;
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;
y = a;
}
});
one.start();other.start();
one.join();other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
//System.out.println(result);
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
运行结果:
第12985次 (0,0)
结果显示:第12985次出现了x=0&&y=0的情况(我反正运行这个程序等了很久),所以有指令重排序现象发生。
但是多线程会出现线程安全问题,所以要禁止指令重排序。(为什么指令重排序会出现线程安全问题?这也是十分重要的问题,我们第3节会细讲)
java源码对变量加volatile
JVM加内存屏障。而JVM是跑在操作系统上的,那么底层是怎么实现的?
hotspot实现 lock; addl
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。
orderaccess_linux_x86.inline.hpp
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
Lock多用于在多处理器中执行指令时对共享内存的独占使用。
它的作用是能够将当前处理器对应缓存的内容删除到内存,并使其他处理器对应的缓存失效。
另外还提供了有序的指令无法越过这个内屏障的作用。
饿汉模式(没有啥花里胡哨的)
public class Girlfiriend extends Friend{
private static final Girlfiriend GF=new Girlfiriend ();
private Girlfiriend (){
}
public static Girlfiriend getGirlfriend(){
return GF;
}
}
懒汉模式
public class Girlfiriend extends Friend{
private static final Girlfiriend GF;
private Girlfiriend (){
}
public static synchronized Girlfiriend getGirlfriend(){
if(GF==null){
GF = new Girlfriend();
}
return GF;
}
}
但是这种做法有一个缺点,不管是不是已经存在实例了,都会被锁阻塞。
妄图通过减少同步代码块的方式提高效率,实际上不可行:
public class Girlfiriend extends Friend{
private static volatile Girlfiriend GF;
private Girlfiriend (){
}
public static Girlfiriend getGirlfriend(){
if(GF==null){//第一个线程判断为null,进来;第二个线程判断也是null,进来
//第一个线程锁住,创建了新的对象。----第二个线程获得锁对象,又新创建了一个对象
synchronized(Girlfiriend.class){
GF = new Girlfriend();
} //第一个线程执行完,释放锁对象,第二个线程执行;
}
return GF;
}
}
改进:双重检验锁
public class Girlfiriend extends Friend{
private static final Girlfiriend GF;
private Girlfiriend (){
}
public static Girlfiriend getGirlfriend(){
if(GF==null){//第一个线程判断为null,进来;第二个线程判断也是null,进来
//第一个线程锁住,创建了新的对象。----第二个线程获得锁对象
synchronized(Girlfiriend.class){
if(GF==null){//第一个线程再次判断是否为null,为null创建新对象;--第二个线程判断是否为null
GF = new Girlfriend();
}
} //第一个线程执行完,释放锁对象,第二个线程执行;
}
return GF;
}
}
所以为什么要加 volatile
呢?为什么指令重排序会产生问题呢?下面从对象创建过程来形象理解一下。
类加载过程就不展开了,简而言之就是:加载、链接(验证、准备、解析)、初识化过程。注:解析往往是发生在初始化之后的。
如果你对类加载过程不了解,可以看我之前写的文章:JVM类加载子系统
下面这幅图就很好的说明了指令重排序会出现线程安全问题:
准备、初识化、解析——》准备、解析、初识化
volatile关键字最主要的作用是:
可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
原子性指一个操作不能被打断,要么全部执行完毕,要么不执行。
参考B站马士兵:《Java多线程与高并发》