浅谈volatile关键字

2019-05-24 06:11:51来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

volatile是一种轻量级的同步机制。它可以保证内存可见性以及防止指令重排序,但是不保证原子性

volatile和JMM机制是不可分割的,在谈volatile的时候有必要先了解以下JMM

JMM(Java内存模型)

JMM是一种抽象的概念模型,实际上并不存在。JMM主要可以将内存看成两块,一块是主内存,用于存放共享变量,线程间共享。

一块是线程私有的工作内存,存放变量副本。每次线程生成的时候都会创建一个私有的工作内存。当线程要操作主内存中的共享

变量的时候需要先将变量复制到自己的工作内存中,在工作内存中对变量副本进行操作,操作完成后再同步回主内存。

简单了解了JMM后我们就深入了解一下volatile是如何保证内存可见性,禁止指令重排序,又是为什么不保证原子性

内存可见性

由JMM模型我们可以看到,每个线程都是再各自的工作内存中进行工作,它们只知道自己把变量改成什么样了,并不知道其他线程把

变量改成什么样子了。这样会出现一种问题:假设主内存中有变量a的值为0,线程A读取变量a并且copy一份副本到自己的工作内存,

线程B也读取变量a且cope一份副本到自己的工作内存,线程A给变量a加上10,线程B给变量a加上20。那么我们期望的结果是最终主

内存中的变量a的值被同步成了30.但是由于线程A和线程B并不知道对方所作的修改,必定有一方将自己的变量副本同步进主内存的时

侯覆盖掉了另外一放的结果,主内存中变量a的值只会是10或者20。如下图所示。

内存可见性就是每个线程可以感知到其他线程对该变量所做的修改,操作该变量时都会从主内存中取最新的值。还是拿上图的例子来说,

假设线程A对工作内存中的变量a操作完并且通过回主内存后,线程B立马感知该变化,然后从主内存中取出最新的变量a的值,即10,然后对

10加上20然后同步回主内存,那么最终结果就正确了。内存可见性就是一个线程对共享变量做出改变后其他线程能够立即感知该变化,并且从

主内存中获取最新值,然后进行操作。

 不保证原子性

那么volatile每次都是从主内存中获取最新的值进行操作为什么不保证原子性呢,每次都获取最新的值去操作那么结果不就肯定正确的吗。其实不然,

在这里我们要明确一个概念,每次线程在对工作内存中的变量副本操作完后要同步回主内存的时候,一时只能有一个线程同步,如果有多个线程要

往主内存中同步,也只有一个会成功,其他线程会被阻塞在外面,并且挂起。是不是很像对主内存上了一把锁呢。

对于i++这种操作,其实底层是被分成了三个指令来执行。

1 从主内存中拿到值到工作内存

2 对i进行累加

3 将累加后的值写入主内存

考虑这么一种情况,线程A和线程B同时对副本变量操作完了,并且都要同步回主内存,这时候只有一个线程能够通过成功,假设线程A成功获得了主

内存的操作权,线程B就被挂起,当线程A同步完毕后,我们都知道cpu速度是超级快的,这时线程B还没被通知到该变量已被更新时线程B就将变量

同步到主内存并且覆盖掉线程A的修改了。因此volatile不保证原子性。

要想保证原子性可以对累加操作上锁,或者使用atomic原子类

防止指令重排序

我们编写的代码都是被编译成字节码文件然后再去执行的,为了加快程序运行,编译器对指令进行了重排序,程序执行的顺序并不是和我们代码写的顺

序是一样的。比如 int a = 10,b = 20;我们期望的是a先赋值,b再赋值,但是最终执行的时候可能因为指令从排序导致了b先赋值,a后赋值。指令重排序

的前提是数据间不存在数据依赖性。在单线程环境中,不管指令如何重排序,编译器都会保证最后执行结果的正确性。但是在多线程的情况下,可能会出现

各个程序乱序执行,各个线程数据产生了不一致性,运行结果出错等问题。volatile通过加内存屏障进行指令重排序

 

内存可见性代码验证

/**
 *  可见性验证
 * @author chen
 *
 */

class MyDate{
    //没有加volatile,线程A对date的修改没有通知到其他线程,主线程陷入死循环
    //private int date = 0;
    
    //加了volatile,线程A对date的修改通知到其他线程,主线程会更新自己的变量副本为最新值,不会陷入死循环
    private volatile int date = 0;
    
    public void setDate(int date) {
        this.date = date;
    }
    
    public int getDate() {
        return date;
    }
}

class MyThread implements Runnable{
    
    private MyDate myDate;
    
    public void setMyDate(MyDate myDate) {
        this.myDate = myDate;
    }
    
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"启动了。。。");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myDate.setDate(10);
        System.out.println(Thread.currentThread().getName()+"结束了,date数据为:"+myDate.getDate());
    }
}

public class KeJianXingTest {
    
    public static void main(String[] args) {
        MyDate date = new MyDate();
        
        MyThread myThread = new MyThread();
        myThread.setMyDate(date);
        new Thread(myThread,"线程A").start();
        
        while(date.getDate()==0) {
            //如果一直死循环就说明线程A对date的修改没有通知到主线程,及主线程工作
            //空间持有的变量副本还是date = 0
        }
        
        System.out.println("主线程结束,date数据为:" + date.getDate());
    }
}

 

不保证原子性代码验证

/**
 *      验证不保证原子性
 * @author chen
 *
 */
class MyDate2{
    volatile int num = 0;
    
    //AtomicInteger atomicInteger = new AtomicInteger();
}

public class YuanZiXingTest {
    
    public static void main(String[] args) {
        MyDate2 myDate2 = new MyDate2();
        
        for(int i = 1;i<=10;i++) {//10个线程,每个线程对num+1000,结果应该为10000
            new Thread(()->{
                for(int j = 0;j<1000;j++) {
                    myDate2.num++;
                    //myDate2.atomicInteger.getAndIncrement();
                }
            },"线程" + i).start();
        }
        
        while(Thread.activeCount()>2) {//保证10个线程都执行完毕
            Thread.yield();
        }
        System.out.println(myDate2.num);//结果<=10000
    }
}

 


原文链接:https://www.cnblogs.com/czsy/p/10917611.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:新手转行学java难吗?新手学java需要注意的6个方面!

下一篇:spring 组件基于注解的注册方式