就象大家更熟悉的const一样,volatile是一个类型修饰符(type specifier)。它是被设计用来修饰被不同线程访问和修改的变量。如果没有volatile,基本上会导致这样的结果:要么无法编写多线程程序,要么编译器失去大量优化的机会。
推荐一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是真正懂得volatile完全的重要性。
1). 一个参数既可以是const还可以是volatile吗?解释为什么。
2). 一个指针可以是volatile 吗?解释为什么。
3). 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
1). 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2). 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3). 这段代码的有个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
讲讲我的理解: (欢迎打板子...~~!)
关键在于两个地方:
1. 编译器的优化 (请高手帮我看看下面的理解)
在本次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;
当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致
当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致
当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致
举一个不太准确的例子:
发薪资时,会计每次都把员工叫来登记他们的银行卡号;一次会计为了省事,没有即时登记,用了以前登记的银行卡号;刚好一个员工的银行卡丢了,已挂失该银行卡号;从而造成该员工领不到工资
员工 -- 原始变量地址
银行卡号 -- 原始变量在寄存器的备份
2. 在什么情况下会出现(如1楼所说)
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
补充: volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人;
“易变”是因为外在因素引起的,象多线程,中断等,并不是因为用volatile修饰了的变量就是“易变”了,假如没有外因,即使用volatile定义,它也不会变化;
而用volatile定义之后,其实这个变量就不会因外因而变化了,可以放心使用了; 大家看看前面那种解释(易变的)是不是在误导人
------------简明示例如下:------------------
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
使用该关键字的例子如下:
int volatile nVint;
>>>>当要求使用volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
例如:
volatile int i=10;
int a = i;
...
//其他代码,并未明确告诉编译器,对i进行过操作
int b = i;
>>>>volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在b中。而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。
>>>>注意,在vc6中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响:
>>>>首先,用classwizard建一个win32 console工程,插入一个voltest.cpp文件,输入下面的代码:
>>
#i nclude <stdio.h>
void main()
{
int i=10;
int a = i;
printf("i= %d",a);
//下面汇编语句的作用就是改变内存中i的值,但是又不让编译器知道
__asm {
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i= %d",b);
}
然后,在调试版本模式运行程序,输出结果如下:
i = 10
i = 32
然后,在release版本模式运行程序,输出结果如下:
i = 10
i = 10
输出的结果明显表明,release模式下,编译器对代码进行了优化,第二次没有输出正确的i值。下面,我们把 i的声明加上volatile关键字,看看有什么变化:
#i nclude <stdio.h>
void main()
{
volatile int i=10;
int a = i;
printf("i= %d",a);
__asm {
mov dword ptr [ebp-4], 20h
}
int b = i;
printf("i= %d",b);
}
分别在调试版本和release版本运行程序,输出都是:
i = 10
i = 32
这说明这个关键字发挥了它的作用!
------------------------------------
volatile对应的变量可能在你的程序本身不知道的情况下发生改变
比如多线程的程序,共同访问的内存当中,多个程序都可以操纵这个变量
你自己的程序,是无法判定合适这个变量会发生变化
还比如,他和一个外部设备的某个状态对应,当外部设备发生操作的时候,通过驱动程序和中断事件,系统改变了这个变量的数值,而你的程序并不知道。
对于volatile类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,而不会利用cache当中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化——显然也是因为它的数值随时都可能变化的情况。
--------------------------------------------------------------------------------
典型的例子
for ( int i=0; i<100000; i++);
这个语句用来测试空循环的速度的
但是编译器肯定要把它优化掉,根本就不执行
如果你写成
for ( volatile int i=0; i<100000; i++);
它就会执行了
volatile的本意是“易变的”
由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化。比如:
static int i=0;
int main(void)
{
...
while (1)
{
if (i) dosomething();
}
}
/* Interrupt service routine. */
void ISR_2(void)
{
i=1;
}
程序的本意是希望ISR_2中断产生时,在main当中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此
可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远也不会被
调用。如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实
现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。
volatile是什么
下面我们来一个个说明。
考虑下面的代码:
代码:
class Gadget
{
public:
void Wait()
{
while (!flag_)
{
Sleep(1000); // sleeps for 1000 milliseconds
}
}
void Wakeup()
{
flag_ = true;
}
...
private:
bool flag_;
};
上面代码中Gadget::Wait的目的是每过一秒钟去检查一下flag_成员变量,当flag_被另一个线程设为true时,该函数才会返回。至少这是程序作者的意图,然而,这个Wait函数是错误的。
假设编译器发现Sleep(1000)是调用一个外部的库函数,它不会改变成员变量flag_,那么编译器就可以断定它可以把flag_缓存在寄存器中,以后可以访问该寄存器来代替访问较慢的主板上的内存。这对于单线程代码来说是一个很好的优化,但是在现在这种情况下,它破坏了程序的正确性:当你调用了某个Gadget的Wait函数后,即使另一个线程调用了Wakeup,Wait还是会一直循环下去。这是因为flag_的改变没有反映到缓存它的寄存器中去。编译器的优化未免有点太……乐观了。
在大多数情况下,把变量缓存在寄存器中是一个非常有价值的优化方法,如果不用的话很可惜。C和C++给你提供了显式禁用这种缓存优化的机会。如果你声明变量是使用了volatile修饰符,编译器就不会把这个变量缓存在寄存器里——每次访问都将去存取变量在内存中的实际位置。这样你要对Gadget的Wait/Wakeup做的修改就是给flag_加上正确的修饰:
class Gadget
{
public:
... as above ...
private:
volatile bool flag_;
};
在Java中设置变量值的操作,除了long和double类型的变量外都是原子操作,也就是说,对于变量值的简单读写操作没有必要进行同步。
这在JVM 1.2之前,Java的内存模型实现总是从主存读取变量,是不需要进行特别的注意的。而随着JVM的成熟和优化,现在在多线程环境下volatile关键字的使用变得非常重要。
在当前的Java内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,只需要像在本程序中的这样,把该变量声明为volatile(不稳定的)即可,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。
Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。
这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。
而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
由于使用volatile屏蔽掉了VM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
分享到:
相关推荐
volatile修饰符用于声明一个对象可以被外部进程(操作系统、硬件或并发进程等)改变。 1. 声明volatile变量 volatile修饰的变量可以被不同的线程访问和修改,常用于像中断处理程序之类的异步进程进行内存单元访问...
2. volatile修饰符不能与const修饰符同时使用,因为volatile修饰符意味着变量的值可能会改变,而const修饰符意味着变量的值不能改变。 3. 使用volatile修饰符可能会影响编译器的优化,因此需要根据实际情况进行选择...
在C语言编程中,`volatile`是一个非常关键的修饰符,它主要用来处理那些可能会被非预期因素(如中断服务程序、多线程环境或其他进程)改变的变量。`volatile`关键字的存在是为了通知编译器,它后面的变量的值可能会...
4. **volatile**:volatile修饰符确保多线程环境下的可见性和有序性。它用于标记共享变量,确保当一个线程修改了这个变量时,其他线程能够立即看到变化。 5. **synchronized**:同步修饰符,用于方法或代码块,确保...
volatile 访问修饰符指出可能有多个线程修改此变量,要求编译器优化以保证对此变量的修改能够被正确的处理。 方法访问修饰符 方法访问修饰符可以分为四种:public、protected、private 和缺省(无访问修饰符)。 ...
3. 避免死循环:如内容中所述,如果没有volatile修饰符,在循环中检查某个变量的状态时,编译器可能会重复使用寄存器中的值,而不是每次都从内存中读取。这可能导致程序无法正确响应硬件状态的变化,比如,永远无法...
本文将详细介绍C语言中的三种重要类型修饰符:`const`、`static` 和 `volatile`。 #### 二、`const`修饰符详解 `const`修饰符主要用于声明一个或多个变量为常量,即一旦赋值之后,其值不能被修改。`const`修饰符...
- **volatile**: 易失修饰符,用于多线程环境,保证变量在多个线程之间的可见性,但不保证原子性。 - **transient**: 暂时性修饰符,表明变量不参与序列化,不会被持久化到磁盘。 3. **方法的控制修饰符** - **...
在Java编程语言中,修饰符是用来限制或指定代码元素(如类、方法、变量)的访问权限、特性或行为的关键词。以下是对标题和描述中提及的Java修饰符及其相关知识点的详细介绍: 1. 类的访问控制修饰符: - **public*...
Java中的访问修饰符是编程中控制类、接口、方法和字段访问权限的关键元素。它们用于确定哪些代码可以访问特定的程序元素。以下是Java访问修饰符的详细解析: ...在多线程编程中,volatile修饰符可以帮助解决并发问题。
因此,对于多线程共享的变量,使用volatile修饰符可以防止编译器优化导致的线程安全问题,确保线程在访问这些变量时总能够获取到最新的值。 除了这些应用场景,volatile关键字还常与其他修饰符一起使用,例如const...
根据 JAVA 访问修饰符的特性和使用范围,我们可以将其分为三类:修饰符类的访问修饰符、属性的访问修饰符和方法的修饰符。 一、修饰符类的访问修饰符 修饰符类的访问修飾符共有四种:默认、public、abstract 和...
static:修饰符:修饰变量,函数。作用域:变量仅仅在本文件可见,函数在本文件可以被调用。static在函数内部定义的话,分配在堆中,数值保存在data段,而不是在栈中,而且只赋值一次。 extern:修饰符:修饰变量,...
Java是一种面向对象的编程语言,其中修饰符和访问修饰符是控制代码访问权限和行为的重要元素。本篇文章将详细介绍Java中的各种修饰符和访问修饰符。 1. **类的访问修饰符** - `public`:任何类都可以访问声明为`...
Java还支持非访问控制修饰符,如`static`、`final`、`abstract`、`volatile`和`synchronized`等,它们不直接影响成员的可见性,而是用于控制成员的行为特性,如静态绑定、不可变性、抽象化、线程同步等。 ##### ...
当一个字段被volatile修饰时,它的值会立即更新并可见于所有线程。 - **transient**:瞬态修饰符,用于声明不参与序列化的字段。当你序列化一个对象时,transient字段不会被保存到序列化的文件中。 在实际开发中,...
- `volatile`:挥发性修饰符,用于标记变量,确保多个线程之间的可见性并防止指令重排序。 理解并熟练运用Java修饰符是编写高质量、可维护的代码的基础。它们帮助开发者控制类和成员的可见性,保证数据安全,实现...
在Java编程语言中,`volatile`关键字是一个非常重要的并发控制机制,它被用来修饰类的成员变量,确保这些变量在多线程环境下的可见性和有序性。然而,使用`volatile`并非总是如我们所期望的那样简单,有时会出现一些...
### Java修饰符深入解析 #### 引言 Java作为一种广泛使用的面向对象编程语言,其丰富的修饰符机制为程序设计提供了灵活性和控制力。本文旨在全面剖析Java中的修饰符,包括它们的特点、用法以及如何在不同场景下...
C#修饰符 ...* volatile:易失 ( 共享 ) 域修饰符。 * transient:暂时性域修饰符。 通过学习C#和Java语言中的修饰符,我们可以更好地理解和使用这些关键字,从而编写更加高效、安全、灵活的程序。