浅析volatile原理及其使用

Posted itjun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析volatile原理及其使用相关的知识,希望对你有一定的参考价值。

前言

经常在网上看一些大牛们的博客,从中收获到一些东西的同时会产生一种崇拜感,从而萌发了自己写写博客的念头.然而已经有这个念头很久,却始终不敢下手开始写.今天算是迈出了人生的一大步^_^!


volatile的定义及其实现

定义:如果一个字段被声明成volatile,那么java线程内存模型将确保所有线程看到的这个变量的值都是一致的.

从它的定义当中咱们也可以了解到volatile具有可见性的特性.但它具体是如何保证其可见性的呢?

先看一段JIT编译器生成的汇编指令

//Java代码如下
instance = new Singleton(); //这里instance是volatile变量
//反汇编后
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock add1 $0x0,(%esp);

有volatile修饰的变量在进行写操作时会出现第二行反汇编代码,重点在lock这个指令.它有两个目的:

  1. 立即回写当前处理器缓存行的值到内存.
  2. 其他所有cpu缓存了该地址的数据将会失效.

这里大家也许会有疑问,有没有可能存在多个cpu一起回写数据?

答案是不会的.虽然cpu鼓励多个处理器可以有竞争,但是总线会对竞争做出裁决,只会有一个cpu获取优先权.其他处理器会被总线禁止,处于阻塞状态.如下图:

技术分享图片

对于第二点,其他cpu缓存该地址的数据失效后想要再次使用的话就必须得从主内存中重新读取,这样就能保证再次执行计算时所获取的值是最新的,也可以认为所有CPU的缓存是一致的,这也就证明了volatile修饰的字段是可见的.


可见性不代表在并发下是安全的

这里咱们先引进一段代码:

/**
 * volatile 变量自增运算
 *
 * @author mars
 */
public class VolatileTest {
    public static volatile int count = 0;

    public static void increase() {
        count++;
    }

    private static final int THREAD_COUNTS = 20;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(THREAD_COUNTS);
        Thread[] threads = new Thread[THREAD_COUNTS];
        for (int j = 0; j < THREAD_COUNTS; j++) {
            threads[j] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                    latch.countDown();
                }
            });
            threads[j].start();
        }
        //等待所有的线程执行结束
        latch.await();

        System.out.println(count);
    }
}

这段代码供发起了20个线程,对count变量进行了10000次自增操作,如果volatile修饰的字段在并发下是安全的话,讲道理最终结果都会是200000,但经过测试发现,每次的输出结果都会不一样.但具体是什么原因造成的?

其实最主要的问题是出在increase()这个自增方法上,这个操作不是一个原子操作,也就是不是一步就能操作完成的,其中会经历count值入栈,add,出栈,到操作线程缓存,最终到内存等等一系列步骤.当A线程其执行这些指令时,B线程正好将数据同步到了主内存中,此时A线
程栈顶的数据就会变成过期数据,然后A线程就会将较小的值同步到主内存中.

技术分享图片


如何正确的运用volatile

要想运用好volatile修饰符,需要保证运用场景符合下述规则:

  1. 运算结果不依赖变量的当前值.
  2. 该变量不需要和其他变量共同参与约束.

例如使用volatile变量来控制并发就很合适:

    volatile boolean shutdownWork;

    public void shutdowm(){
        shutdownWork = true;
    }

    public void doWork(){
        while (!shutdownWork){
            //execute task
        }
    }

上面这段代码运行结果并无需依赖shutdownWork的值,但是只要shutdownWork的值一旦经过改变,便会立即被其他所有线程所感知,然后停止执行任务.


小知识点

在多处理器下,为了保证各个处理器的缓存是一致的,处理器会使用嗅探技术来保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致.如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存无效,在下次访问相同的内存地址时,强制执行缓存行填充,也就是从内存中重新读取该内存地址指向的值.

End


以上是关于浅析volatile原理及其使用的主要内容,如果未能解决你的问题,请参考以下文章

JAVA并发编程:volatile的使用及其原理

Java 并发编程:volatile的使用及其原理

深入理解java:2.1. volatile的使用及其原理

浅析Spring MVC的工作原理及其与Spring的关系

springboot返回统一数据格式及其原理浅析

cglib动态代理实现及其原理浅析