【Java并发】- volatile详解

本文详细介绍了Java中volatile关键字的特性,包括可见性、原子性以及其在内存中的语义。volatile保证单个变量的读写具有原子性,但不适用于复合操作。此外,文章通过双重检查锁示例解释了volatile在并发编程中的正确使用场景,强调volatile并不等同于同步锁,应谨慎使用。

在这之前需要先了解java内存模型

volatile特性

  • 可见性。对一个volatile变量的读,总能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性。对任意一个volatile单个变量的读/写具有原子性。

    但是类似于volatile++这种不具有原子性。因为volatile只保单个证读写具有原子性,这里的volatile++相当于1)读volatile的值,2)volatile + 1, 3)将更新的值赋值给volatile变量。其中包含了多个的操作,所以不具备原子性。

    通俗的讲,单个的volatile变量的读/写可以理解为加了一个锁
    例如:

public volatile int a = 0;
public void set(){
    a = 1;
}

这里可以理解为:

public int a = 0;
public synchronized void set(){
    a = 1;
}

volatile的happens-before关系

首先需要理解重排序的概念,简单介绍重排序:
例:

private int a = 0;

        private boolean isSet = false;

        public void set(){
            a = 1;            //1
            isSet = true;     //2
        }

        public int get(){
            if(isSet)              //3
                return a * a;      //4
        }

现在假设有一个线程A先执行set,另一个线程B再执行get,当执行set的时候,在单线程中1和2的实际执行顺序是不被保证的,因为编译器或者处理器为了能经可能快的处理,会适当的进行指令的重排序,前提是在没有指令依赖的情况下,但是保证执行的结果不受影响。也就是说有可能实际执行的顺序是先执行2再执行1,但是放在多线程中则成了问题,当A线程先执行了2的时候,B线程看到isSet是true,这个时候去读取了a,但是这个时候A线程中还有执行1.所以导致问题的出现。

private int a = 0;

        private volatile boolean isSet = false;

        public void set(){
            a = 1;
            isSet = true;
        }

        public int get(){
            if(isSet)
                return a * a * a * a;
            return 1;
        }

把变量换成volatile便不会有问题。

volatile保证在volatile变量操作之前的指令不会被重排序到volatile变量之后,volatile变量之后的指令保证不会被重排序到volatile变量操作之前。在这个例子中就是:1 happens-before 2, 3 hahappens-before 4.同时很明显2 hahappens-before 3根据传递性便可以得出1 hahappens-before 3所以不会发生问题。

volatile的内存语义

  • 读:当读一个volatile变量的时候,JMM(java内存模型)会把该线程对应的本地内存置为无效。线程将从主存中读取共享变量。
  • 写:当写一个volatile变量的时候,JMM会把线程对应的本地内存的共享变量刷新到主存。

volatile变量的读/写都会去锁住总线或者缓存,就像是加了一把锁,说白了就是少去了线程自身的变量副本的读写的操作,直接去读写主存。所以内存是可见的。

volatile使用场景

以经典的双重检查锁为例。

看一个简单的单例模式(懒汉式):

public class Singleton {

    private Singleton instance = null;

    private Singleton(){}

    public Singleton getInstance(){
        if(instance == null)             //1
            instance = new Singleton();  //2
        return instance;
    }
}

这个单例模式在单线程中是没有问题的,但是如果是多线程都在调用getInstance方法的时候,便会有问题。原因在执行2的过程可以分为三步:1)分配对象的内存空间 2)初始化对象 3)将instance引用指向分配的内存地址。同时这三个操作还可能被重排序为:1)分配对象的内存空间 2)将instance引用指向分配的内存地址 3)初始化对象。当A线程执行到1的的时候B线程执行到2,也就是对象还没有完成初始化,A线程就会进一步也去执行2,从而失去了单例模式的意义。

当然最简单的办法就是在getInstance方法上加上synchronized关键字变成同步代码块。但是如果有很多线程都调用getInstance方法的话synchronized对于性能的开销很大,所以就出现了双重检查锁的写法:

private DoubleCheckedLocking instance = null;

    private DoubleCheckedLocking(){}

    public DoubleCheckedLocking getInstance(){
        if(instance == null){
            synchronized(DoubleCheckedLocking.class){
                if(instance == null)
                    instance = new DoubleCheckedLocking();  //1
            }
        }
        return instance;
    }
}

这个方案近乎Perfect,但是有一点,那就是当A线程执行到1的时候,B线程判断instance不为null所以直接返回instance,但是由于上面说的编译重排序的问题,可能导致instance确实存了对象的内存地址,但是这个时候这个对象还没有完成初始化。
问题的根源就出在了对于instance变量的写因为重排序而导致没有内存可见性,因为使用volatile:

public class DoubleCheckedLocking {

    private volatile DoubleCheckedLocking instance = null;

    private DoubleCheckedLocking(){}

    public DoubleCheckedLocking getInstance(){
        if(instance == null){
            synchronized(DoubleCheckedLocking.class){
                if(instance == null)
                    instance = new DoubleCheckedLocking();
            }
        }
        return instance;
    }
}

这样volatile保证volatile变量的写happens-before volatile变量的读,就没有问题了。
相当于在instance的写处加了一个锁:

public class DoubleCheckedLocking {

    private volatile DoubleCheckedLocking instance = null;

    private Object lock = new  Object();

    private DoubleCheckedLocking(){}

    public DoubleCheckedLocking getInstance(){
        if(instance == null){
            synchronized(DoubleCheckedLocking.class){
                if(instance == null){
                    synchronized(lock){
                        instance = new DoubleCheckedLocking();
                    }
                }   
            }
        }
        return instance;
    }
}

正确使用volatile

volatile很容易被误解为是原子类型,或者被误认为和synchronized一样,而且效率更高。实际上volatile只是保证对于volatile变量的读/写具有原子性,但是对于复合型操作(例如:volatile++)不保证原子性。所以volatile变量如果不是很了解的话慎用。

下面简单总结使用场景:

  • 对字段的写操作不依赖于当前值。(例如:volatile++则不符合)
  • 只有一个线程在写这个volatile变量,多个线程读的情况可以使用。
  • 用作标志位(valotile boolean flag)

声明一个volatile的引用变量,不能保证通过该引用变量访问到的非volatile变量的可见性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值