volatile到底是个什么鬼--详解

问题:退不出的循环

先看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止

static boolean run=true;
public static void main(String[] args) throws InterruptedException{
    Thread t=new Thread(()->{
        while(run){
            //...
        }
    });
    t.start();
    sleep(1);
    run=false;  //主线程中将run的值改为false,线程t中的while循环会不会如预想的停止???
}

答案是:不会,主线程中将run的值改为false,线程t中的while循环不会如预想的停止。

为什么呢?分析:

1)初始状态,t线程刚开始从主内存中读取了run的值到工作内存中。

 2)因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存到自己的工作内存中的高速缓存中,减少对主存中run的访问,提高效率。

3)1秒之后,main线程改变了run的值,并同步到主内存中,而t线程是从自己工作内存中的高速缓存中读取的run的值,结果永远是旧的值。

解决方法--volatile

volatile(易变关键字):它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存查找变量的值,必须从主存中读取变量的值。

上述例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况,上例从字节码理解是这样的:

注意:synchronized语句块可以保证代码块的原子性和可见性,但是缺点是synchronized是属于重量级操作,性能相对比较低。

以上是说到了volatile保证了共享变量的可见性,但是volatile还有一个重要作用就是保证有序性,那么有序性到底是什么???

有序性

JVM会在不影响代码结果的正确性的前提下,可以调整语句的执行顺序,思考以下一段代码:

static int i;
static int j;
//在某个线程内执行如下赋值操作
i=...;
j=...;

可以看到,无论是先执行i还是先执行j,对最终结果不会产生影响。所以,上面的代码在真正执行的时候既可以是

i=...;
j=...;

也可以是

j=...;
i=...;

这种特性称之为指令重排,多线程下的指令重排会影响正确性。但是为什么会有指令重排这种现象??在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在80-90年代占据了计算机架构的重要地位。

指令重排之诡异的结果

int num=0;
boolean ready=false;
//线程1执行此方法
public void actor1(I_Result r){
    if(ready){
        r.r1=num+num;
    }else{
        r.r1=1;
    }
}
//线程2执行此方法
public void actor2(I_Result r){
    num=2;
    ready=true;
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

情况1:线程1先执行,这个时候ready=false,所以进入else分支,结果为1

情况2:线程2先执行num=2,但是还没来得及执行ready=true,线程1执行,还是进入else分支,结果为1

情况3:线程2执行到ready=true,线程1执行,这回进入id分支,结果为4(因为num=2已经执行过了)

但是还有一种情况结果为0!!!!

这种情况下是:线程2先执行ready=true,切换到线程1,进入if分支,相加为0,再切换为线程2执行num=2!!!这个时候是线程2在执行actor2()方法中的两条语句时发生了指令重排。

那么这种情况的解决方法还是volatile,volatile修饰的变量,可以禁止指令重排!!!!

volatile原理

volatile的底层实现原理是内存屏障:

  • 对volatile变量的写指令后会加入写屏障
  • 对volatile变量的读指令前会加入读屏障

如何保证可见性

写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public void actor2(I_Result r){
    num=2;
    ready=true;   //ready是volatile赋值带写屏障
    //写屏障
}

而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

public void actor1(I_Result r){
    //读屏障
    //ready是volatile读取值带读屏障
    if(ready){
        r.r1=num+num;
    }else{
        r.r1=1;
    }
}

如何保证有序性

  • 写屏障会确保指令重排时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排时,不会将读屏障之后的代码排在读平展之前

还是那句话,不能解决指令交错

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面
  • 而有序性的保证也只是保证了本线程内相关的代码不被重排

double-checked locking(双重检验锁)问题

以著名的double-checked locking单例模式为例

public final class Singleton{
    private Singleton(){}
    private static Singleton INSTANCE=null;
    public static Singleton getInstance(){
        if(INSTANCE==null){
            // 首次访问会同步,而之后的使用没有synchronized
            synchronized(Singleton.class){
                if(INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
    }
}

以上实现的特点是:

  • 懒惰实例化
  • 首次使用synchronized()才使用synchronized加锁,后续使用时无需加锁
  • 有隐含的,但是很关键的一点:第一个if使用了INSTANCE变量,是在同步块之外的

在多线程环境下,上面的代码是有问题的,getInstance()方法中代码对应的字节码为:

其中

  • 17表示创建对象,将对象引入栈  //new Singleton
  • 20表示复制一份对象引用  //引用地址
  • 21表示利用一个对象引用,调用构造方法
  • 24表示利用一个对象引用,赋值给static INSTANCE

也许jvm会优化为:先执行24,再执行21。如果两个线程t1,t2按如下时间序列执行

关键在于0:getstatic这行代码在monitor控制之外,这个时候t1还没有完全将构造方法执行完毕,如果在构造方法中执行要很多初始化操作,那么t2拿到的将是一个未初始化完毕的单例。

对INSTANCE使用volatile修饰即可,可以禁用指令重排,但是要注意在JDK 5以上的版本的volatile才会真正有效。

double-checked locking解决

对INSTANCE使用volatile修饰即可:

public final class Singleton{
    private Singleton(){}
    private static volatile Singleton INSTANCE=null;
    public static Singleton getInstance(){
        if(INSTANCE==null){
            // 首次访问会同步,而之后的使用没有synchronized
            synchronized(Singleton.class){
                if(INSTANCE==null){
                    INSTANCE=new Singleton();
                }
            }
        }
    }
}

读写volatile变量的时候会加入内存屏障,保证下面两点:

  • 可见性

写屏障保证在该屏障之前的线程对共享变量的改动都同步到主存中

而写屏障保证在该屏障之后的线程对共享变量的读取,加载的是主存中最新的数据

  • 有序性

写屏障会确保指令重排的时候,不会将写屏障之前的代码排在写屏障之后

读屏障会确保指令重排的时候,不会将读屏障之后的代码排在读屏障之后

更底层的读写变量时使用lock指令(锁总线)来确保多核cpu之间的可见性与有序性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值