volatile详解

volatile摘要

volatile是轻量级的synchronized,它在多线程开发中保证共享变量的“可见性”。恰当的使用volatile修饰变量比使用synchronized执行成本更低,因为它不会引起线程上下文切换和调度。

volatile实用性

volatile是一个轻量级的同步机制关键字,和synchronized关键字挺相似,差别在于synchronized保证了操作的原子性,但volatile不能保证。

volatile作用:

(1)保证可见性:线程将主内存中的数据拷贝到自己的工作内存中进行操作,操作后再将数据写回主内存,一个线程对于数据的改动其他线程立即可感知到;例如开启两个线程,一个线程对于加了volatile的变量进行改变,之后第二个线程会感知到此变量已被改变;

(2)不保证原子性:原子性即完整性,某个线程在做某个业务时,中间不可以被分割,要么同时成功,要么同时失败;注意,i++这样的操作不是原子性操作;例如20个线程,每个线程循环1000次,每次将count值加1,结束后很可能不等于20000;如何解决不保证原子性:使用JUC下的AtomicInteger的getAndIncrement方法,底层原理是CAS;

(3)禁止指令重排:单线程环境中确保程序最终执行结果和代码顺序执行结果一致,多线程环境线程交替执行,由于编译器优化重排存在,两个线程中使用的变量能否保证一致无法确定,结果无法预测;例如如下所示,语句可以按1234顺序执行也可以按1324执行,但不可以按4123执行,因为存在数据依赖性。

int x = 11;
int y = 12;
x = x + 5;
y = x * x;

volatile底层原理

Java虚拟机规范定义了一种Java内存模型(JMM),来屏蔽各种硬件和操作系统的内存访问的差异性。JMM规定所有变量都存储在主内存中,每个线程都包含自身的工作内存,线程都只能访问自身的工作内存,且工作前后要把值同步回主内存。如下图所示。

在线程执行时,首先会从主存中read变量值,再load到工作内存的副本中,然后再传给处理器执行,执行完毕后再给工作内存中的副本赋值,随后工作内存再把值传回给主存,主存中的值才更新。

虽然解决了主内存中各个线程都可见的问题,但由此也带来另外的问题。例如下面的例子。

i = i + 1;

假设初始值i为0,当单个线程执行后i肯定变为1,毋庸置疑。但当两个甚至多个线程执行后,结果会朝着我们预料的方向发展吗?那可不一定,例如下面的例子。

线程1 : load i from 主内存 // i = 0
        i + 1 // i = 1
线程2 : load i from 主内存 // 因为线程1还未将i写回主内存,i = 0
        i + 1 // i = 1
线程1 : save i to 主内存
线程2 : save i to 主内存

以上会发现,两个线程都将i进行加1,但结果却是1而不是2。这也直接表明了volatile不能保证原子性,究其原因就是类似i++这样的操作并非原子性操作,它分为了读取i->加1->写回主内存,一共三步操作而非一步操作。JMM只实现了基本的原子性操作,而像i++这样非原子性操作必须借助synchronized或者Lock来解决。

当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。这也表明了volatile保证了可见性。通过synchronized和Lock也可以保证可见性,但开销较大。

当然,上面已经详细解释了volatile可以禁止指令重排的样例,JMM拥有一些先天的有序性,通过称为happens-before,它的一些规则如下:

(1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作;

(2)监视器锁规则:对一个线程的解锁,happens-before于随后对这个线程的加锁;

(3)volatile变量规则:对一个volatile域的写,happens-before于后续对这个volatile域的读;

(4)传递性:如果A happens-before B,且B happens-before C,那么A happens-before C;

(5)start()规则:如果线程A执行操作ThreadB_start(),那么A线程的ThreadB_start() happens-before于B中的任意操作;

(6)join()原则:如果A执行ThreadB.join()并且成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回;

(7)interrupt()原则:对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生;

(8)finalize()原则:一个对象的初始化完成先行发生于它的finialize()方法的开始。

volatile使用场景

状态标记,这种对变量的读写操作,标记为volatile可以保证修改对线程立刻可见。

int a = 0;
volatile bool flag = false;

public void write() {
    a = 2;              // 1
    flag = true;        // 2
}

public void multiply() {
    if (flag) {         // 3
        int ret = a * a;// 4
    }
}

单例模式下,典型的双重检锁(DCL),下面是一种懒汉模式,使用时才创建对象,而且为了避免初始化操作指令重排序,给instance加上了volatile。

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}


end
  • 作者:JJ(联系作者)
  • 发表时间:2021-02-15 13:06
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载博主转载的文章,请附上原文链接
  • 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  • 评论