volatile 原理

volatile 关键字是Java提供的一种轻量级同步机制。它能够保证可见性有序性,但是不能保证原子性

可见性

每个Java线程都有自己的工作内存。操作数据,首先从主内存中读取数据,得到一份拷贝,操作完毕后再写回到主内存。 这样某个线程对主内存内容的更改,立刻通知到其它线程。

可见性测试:

class MyData{
    //保证多个线程对number的修改其他线程可见
    volatile int number = 0;
    public void add60(){
        this.number = 60;
    }
    public void increNum(){
        number++;
    }
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }
}
public class VolatileDemo {
    public static void main(String[] args) {
        volatileVisibility();
    }
    //可见性
    public static void volatileVisibility() {
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t come in");
            //暂停3秒
            try{
                TimeUnit.SECONDS.sleep(3);
            }catch (Exception e){
                e.printStackTrace();
            }
            myData.add60();
            System.out.println(Thread.currentThread().getName()+"\t update number value:"+myData.number);
        },"A").start();
        while (myData.number==0){
            //当没有使用 volatile时,main线程就一直在这里等待,没有感知到number更改为60
        }
        System.out.println(Thread.currentThread().getName()+"\t mission is over main get number:"+myData.number);
    }
}

一开始number变量没有用volatile修饰,所以程序运行的结果是:

AAA  come in
AAA  update number value: 60

虽然一个线程把number修改成了60,但是main线程持有的仍然是最开始的0,所以一直循环,程序不会结束。

如果对number添加了volatile修饰,运行结果是:

AAA  come in
AAA  update number value: 60
main     mission is over. main get number value: 60

可见某个线程对number的修改,会立刻反映到主内存上。

原子性

volatile 并不能保证操作的原子性。比如一条number++的操作,会形成3条指令。

getfield        //读
iconst_1    //常量1
iadd        //加1操作
putfield    //写回主存

假设有3个线程,分别执行number++,都先从主内存中拿到 number=0,然后三个线程分别进行操作。假设线程0还没有将number=1,写回主存。但是此时线程1、2已经拿到了number=0,在这个时间间隙,发生了写覆盖,线程1、2将number变成1。

在这个过程中 number++ 作为一个原子操作应当不被打断,因此 volatile 不保证原子性。

有序性

volatile 可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,我们写的程序代码被翻译成一条条指令,CPU执行指令的顺序可能跟程序员自己编写的顺序不一致。

int x = 11; //语句1
int y = 12; //语句2
x = x + 5;  //语句3
y = x * x;  //语句4

以上例子,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,最终结果都是x = 16,y=256。但是如果是4开头,就有问题了,y=0。这个时候就不需要指令重排序。

单线程情况不存在指令重排,多线程环境线程交替执行,编译器优化重排的存在,两个线程中使用的变量能否保证一致性无法确定。

volatile底层是用CPU的内存屏障(Memory Barrier)指令来实现禁止指令重排,有两个作用,一个是保证特定操作的顺序性,二是保证变量的可见性。在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

volatile 在单例模式的使用

public class Singleton {
    private static volatile Singleton instance=null;
    private SingletonDemo(){}
    //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
    public static Singleton getInstance(){
        if (instance==null){
            synchronized (Singleton.class){
                 if (instance==null){
                     instance=new Singleton();
                 }
            }
        }
        return instance;
    }

虽然使用双重检索保证了多线程获取的实例是单例的,但是不使用 volatile 修饰 instance 会出现指令重排的现象,instance=new Singleton(); 在编译阶段分为三步:

memory = allocate();     //1.分配内存
instance(memory);     //2.初始化对象
instance = memory;     //3.设置引用地址

 如果2,3操作被重排,将会导致,跳过初始化对象这步,最终返回的 instance 未进行初始化。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值