volatile //你到底对我做了什么?

volatile是一个类型修饰符(type specifier),就像大家更熟悉的const一样,它是被设计用来修饰被不同线程访问和修改的变量。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。这是网上的解释、

你可以这样定义它:

volatile int i = 0;

它表示这个变量 i 是不被编辑器优化的;

什么是优化呢?

比如

int XBYTE[2];

XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;

对部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,如果键入volatile,则编译器会逐一地进行编译并产生相应的机器代码(产生四条代码)。

上面的代码,编译器会对上面的代码做优化处理,最终值是0x58;(即忽略前三条语句,只产生一条机器代码)。

然后将对应XBYTE[ 2 ] 的值copy 到寄存器中,下次使用时,直接从寄存器中取值,而不是从内存原始地址中取值。

这将会产生问题:

static int flag = 0;
int FA(void)
{
    //...
    while(1)
    {
        if(flag)
            dosomething();
    }
}
/*Interruptserviceroutine.*/
void ISR_2(void)
{
    flag  = 1;
}

上述程序解读为,定义静态变量 标志位 flag ,我们期望程序在发生中断后,能执行 dosomething() 函数;

但是实际是,它可能永远都不肯能执行到。例如, 线程一执行FA (); 线程二 执行ISR_2(); 线程一中没有改变flag的代码,所以缓存寄存器中的值永远没有修改;

理解理解再理解

1、原子操作

           原子性

          1.1.概念:要么全部执行,要么全部不执行,

          1.2.操作实现

 举个例子

 volatile int i=1;

 i =2 ; // 写操作,原子性

 int j=i; //读操作,原子性

 i++;  //这个不是,没有达到原子性,包含三个操作,读取i的值,然后进行加1操作,然后进行写入内存,操作可能被打断;

2、volatile 的可见性

 保证可见性: 1.将当前处理器缓存行的数据写回到系统内存
                              2.这个写回的内存操作会使其他CPU里缓存了该内存地址的数据无效。

          如下:当线程A写入主内存后,让线程B本地内存x值无效,让线程B重新去主内存读取数据

确保所有的线程都能看到共享变量的最新值,可以在所有执行读操作或写操作的线程上加上同一把锁。

这是区分C程序员和嵌入式系统程序员的最基本的问题:嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所有这些都要求使用volatile变量。不懂得volatile内容将会带来灾难。

3、volatile关键字修饰的变量不会被指令重排序优化

线程A执行的操作如下:

Map configOptions ;
char[] configText;

volatile boolean initialized = false;

//线程A首先从文件中读取配置信息,调用process...处理配置信息,处理完成了将initialized 设置为true
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfig(configText, configOptions);//负责将配置信息configOptions 成功初始化
initialized = true;

线程B等待线程A把配置信息初始化成功后,使用配置信息去干活.....线程B执行的操作如下:

while(!initialized)
{
    sleep();
}

//使用配置信息干活
doSomethingWithConfig();

如果initialized变量不用 volatile 修饰,在线程A执行的代码中就有可能指令重排序。

线程A执行的代码中的最后一行:initialized = true 有可能重排序到了 processConfig方法调用的前面执行,

这意味着:配置信息还未成功初始化,但是initialized变量已经被设置成true了。那么就导致 线程B的while循环“提前”跳出,拿着一个还未成功初始化的配置信息去干活(doSomethingWithConfig方法)。。。。

因此,initialized 变量就必须得用 volatile修饰。这样,就不会发生指令重排序,也即:只有当配置信息被线程A成功初始化之后,initialized 变量才会初始化为true。综上,volatile 修饰的变量会禁止指令重排序(有序性)
 

那什么是指令重排序?有两个层面:
  • 在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。拿上面的例子来说:假如不是a=1的操作,而是a=new byte[1024*1024](分配1M空间),那么它会运行地很慢,此时CPU是等待其执行结束呢,还是先执行下面那句flag=true呢?显然,先执行flag=true可以提前使用CPU,加快整体效率,当然这样的前提是不会产生错误(什么样的错误后面再说)。虽然这里有两种情况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。
  • 在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。硬件的重排序机制参见《从JVM并发看CPU内存指令重排序(Memory Reordering)》

在volatile字段读或者写的语句中,它前面的数据必定发生在当前语句之前,前面的语句可以发生重排序;同理,在volatile字段操作之后也同样必须发生在这个语句之后,后面的不会管他们的重排序情况。

举个简单例子:

int a,b,c ;
 
volatile int v1=1;
 
volatile int v2 =2;
 
void readAndWrite(){
   a =1;       //1
   b=2;        //2
   c=3;        //3
   int i =v1;   //4 第一个volatile读
   int j =v2;    //5 第二个volatile读
   a = i+j;      // 6 普通写
   b = i+j;       //7
   c = i+j;       //8
   v1=i+1;       //9 第一个volatile写
   v2=j* 2;      //10 第二个volatile写
}

   如上所示:1,2,3可能发生重排序,但是在4的时候,一定会保证1,2,3都是执行了的。同理5,9,10,,都是volatile字段操作都会保证前面的都是执行了的不会让一块发生重排序;同理6,7,8可能发生重排序;

 另外上面的volatile都是单一的读,写,所以都是单一的保证了原子性

注意:

volatile使用条件

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

  • 对变量的写操作不依赖于当前值,例如自增环境下不行 i++;
  • 该变量没有包含在具有其他变量的不变式中。

volatile使用场景

  •      1.volatile特别适合于状态标记量 

          例如: volatile boolean flag = true;

  •     2.双重检查锁定-单例模式使用,经典的例子;
Pubulic Class Singleton {
 
    private volatile static Singleton instance; //1 这里用volatile申明
 
    public static Singleton getInstance() {
        if (null == instance) {
            synchronized (Singleton.class) {
                if (null == instance) {
                    instance = new Singleton ();  //2 这里有三个操作,可能会被重排序
                }
            }
        }
        return instance;
    }
}

   如上所示,单例的引用采用volatile申明,就是为了避免2处的重排序

问题解析:
             instance = new Singleton() 实际是由下面三步完成的。

memory=allocate();              1. //分配对象内存空间
 
ctorInstance(memory);         2.//初始化对象
 
instance=memory;               3.//设置instance指向刚分配的内存

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值