浅析volatile原理及其使用

栏目: Java · 发布时间: 7年前

内容简介:经常在网上看一些大牛们的博客,从中收获到一些东西的同时会产生一种崇拜感,从而萌发了自己写写博客的念头.然而已经有这个念头很久,却始终不敢下手开始写.今天算是迈出了人生的一大步^_^!volatile的定义及其实现定义:如果一个字段被声明成volatile,那么java线程内存模型将确保所有线程看到的这个变量的值都是一致的.

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

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获取优先权.其他处理器会被总线禁止,处于阻塞状态.如下图:

浅析volatile原理及其使用

对于第二点,其他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

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

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

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

volatile boolean shutdownWork;

    public void shutdowm(){
        shutdownWork = true;
    }

    public void doWork(){
        while (!shutdownWork){
            //execute task
        }
    }
复制代码

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

小知识点

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Effective Engineer

The Effective Engineer

Edmond Lau / The Effective Bookshelf, Palo Alto, CA. / 2015-3-19 / USD 39.00

Introducing The Effective Engineer — the only book designed specifically for today's software engineers, based on extensive interviews with engineering leaders at top tech companies, and packed with h......一起来看看 《The Effective Engineer》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具