C# volatile 关键字

volatile

就像你们更熟悉的const同样,volatile是一个类型 修饰符(type specifier)。它是被设计用来修饰被不一样线程访问和修改的 变量。若是不加入volatile,基本上会致使这样的结果:要么没法编写多线程 程序,要么 编译器失去大量优化的机会。
中文名
类型修饰符
外文名
volatile
释    义
易变的
词    性
形容词
属    性
类型 修饰符

做用

编辑
volatile的做用是: 做为指令 关键字,确保本条指令不会因 编译器的优化而省略,且要求每次直接读值.
简单地说就是防止编译器对代码进行优化.好比以下程序:
1
2
3
4
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;
对外部硬件而言,上述四条语句分别表示不一样的操做,会产生四种不一样的动做,可是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。若是键入volatile,则编译器会逐一的进行编译并产生相应的机器代码(产生四条代码).

例子

编辑
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样, 编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都当心地从新读取这个变量的值,而不是使用保存在 寄存器里的备份。下面是volatile变量的几个例子:
1). 并行设备的硬件寄存器(如:状态寄存器)
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
这是区分C程序员和 嵌入式系统程序员的最基本的问题:嵌入式系统程序员常常同硬件、中断、RTOS等等打交道,全部这些都要求使用volatile变量。不懂得volatile内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是否是真正懂得volatile彻底的重要性。
1). 一个参数既能够是const还能够是volatile吗?解释为何。
2). 一个指针能够是volatile 吗?解释为何。
3). 下面的函数被用来计算某个整数的平方,它能实现预期设计目标吗?若是不能,试回答存在什么问题:
1
2
3
4
int  square( volatile  int  *ptr)
{
     return  ((*ptr) * (*ptr));
}
下面是答案:
1). 是的。一个例子是只读的 状态寄存器。它是volatile由于它可能被意想不到地改变。它是const由于程序不该该试图去修改它。
2). 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的 指针时。
3). 这段代码是个恶做剧。这段代码的目的是用来返指针*ptr指向值的平方,可是,因为*ptr指向一个volatile型参数, 编译器将产生相似下面的代码:
1
2
3
4
5
6
7
int  square( volatile  int * &ptr) //这里参数应该申明为引用,否则函数体里只会使用副本,外部无法更改
{
     int  a,b;
     a = *ptr;
     b = *ptr;
     return  a*b;
}
因为*ptr的值可能在两次取值语句之间发生改变,所以a和b多是不一样的。结果,这段代码可能返回的不是你所指望的平方值!正确的代码以下:
1
2
3
4
5
6
long  square( volatile  int *ptr)
{
     int  a;
     a = *ptr;
     return  a*a;
}
讲讲我的理解:
关键在于两个地方:
编译器的优化(请高手帮我看看下面的理解)
在本次线程内,当读取一个变量时,为提升存取速度,编译器优化时有时会先把变量读取到一个寄存器中;之后再取变量值时,就直接从寄存器中取值;
当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致
当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而形成应用程序读取的值和实际的变量值不一致
当该 寄存器在因别的线程等而改变了值,原变量的值不会改变,从而形成应用程序读取的值和实际的变量值不一致
举一个不太准确的例子:
发薪资时,会计每次都把员工叫来登记他们的银行卡号;一次会计为了省事,没有即时登记,用了之前登记的银行卡号;恰好一个员工的银行卡丢了,已挂失该银行卡号;从而形成该员工领不到工资
员工 -- 原始变量地址
银行卡号 -- 原始变量在寄存器的备份
⒉ 在什么状况下会出现
1). 并行设备的硬件寄存器
2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3). 多线程应用中被几个任务共享的变量
补充:volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人;
“易变”是由于外在因素引发的,像多线程,中断等,并非由于用volatile修饰了的变量就是“易变”了,假如没有外因,即便用volatile定义,它也不会变化;
而用volatile定义以后,其实这个变量就不会因外于是变化了,能够放心使用了; 你们看看前面那种解释(易变的)是否是在误导人
volatile 关键字是一种类型 修饰符,用它声明的类型变量表示能够被某些 编译器未知的因素更改,好比:操做系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就再也不进行优化,从而能够提供对特殊地址的稳定访问。
使用该关键字的例子以下:
1
volatile  int  vint;
当要求使用volatile 声明的变量的值的时候,系统老是从新从它所在的内存读取数据,即便它前面的指令刚刚从该处读取过数据。并且读取的数据马上被保存。
例如:
1
2
3
volatile  int  i=10;
int  a=i;
//...
//其余代码,并未明确告诉 编译器,对i进行过操做
1
int  b=i;
volatile 指出 i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,于是编译器生成的汇编代码会从新从i的地址读取数据放在b中。而优化作法是,因为编译器发现两次从i读数据的代码之间的代码没有对i进行过操做,它会自动把上次读的数据放在b中。而不是从新从i里面读。这样一来,若是i是一个 寄存器变量或者表示一个端口数据就容易出错,因此说volatile能够保证对特殊地址的稳定访问。
注意,在vc6中,通常调试模式没有进行代码优化,因此这个 关键字的做用看不出来。下面经过插入汇编代码,测试有无volatile关键字,对程序最终代码的影响:
首先,用classwizard建一个win32 console工程,插入一个voltest.cpp文件,输入下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void  main( int  argc, char  *argv[])
{
     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 关键字,看看有什么变化:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
void  main( int  argc, char  *argv[])
{
     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当中的原有数值,以适应它的未知什么时候会发生的变化,系统对这种变量的处理不会作优化——显然也是由于它的数值随时均可能变化的状况。
--------------------------------------------------------------------------------
典型的例子
1
for ( int  i=0; i<100000; i++);
这个语句用来测试空循环的速度的
可是 编译器确定要把它优化掉,根本就不执行
若是你写成
1
for ( volatile  int  i=0; i<100000; i++);
它就会执行了
volatile的本意是“易变的”
因为访问 寄存器的速度要快过RAM,因此编译器通常都会做减小存取外部RAM的优化。好比:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static  int  i = 0;
int  main( void )
{
     //...
     while (1)
     {
         if (i)
             dosomething();
     }
}
/*Interruptserviceroutine.*/
void  ISR_2( void )
{
     i=1;
}
程序的本意是但愿ISR_2中断产生时,在main当中调用dosomething函数,可是,因为编译器判断在 main函数里面没有修改过i,所以
可能只执行一次对从i到某 寄存器的读操做,而后每次if判断都只使用这个寄存器里面的“i副本”,致使dosomething永远也不会被
调用。若是将变量加上volatile修饰,则 编译器保证对此变量的读写操做都不会被优化(确定执行)。此例中i也应该如此说明。

使用地方

编辑
通常说来,volatile用在以下的几个地方:
一、 中断服务程序中修改的供其它程序检测的变量须要加volatile;
二、多任务环境下各任务间共享的标志应该加volatile;
三、 存储器映射的硬件寄存器一般也要加volatile说明,由于每次对它的读写均可能有不一样意义;
另外,以上这几种状况常常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中能够经过关中断来实现,2 中能够禁止任务调度,3中则只能依靠硬件的良好设计了。

代码

编辑
下面咱们来一个个说明。
考虑下面的代码:
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
classGadget
{
     public :
         void  Wait()
         {
             while (!flag_)
             {
                 Sleep(1000); //sleeps for 1000milli seconds
             }
         }
         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_加上正确的修饰:
1
2
3
4
5
6
7
class  Gadget
{
     public :
         //...as above...
     private :
         volatile  bool  flag_;
};
在Java中设置变量值的操做,除了long和double类型的变量外都是 原子操做,也就是说,对于变量值的简单读写操做没有必要进行同步。
这在JJVM 1.2以前,Java的内存模型实现老是从主存读取变量,是不须要进行特别的注意的。而随着JJVM的成熟和优化,如今在多线程环境下volatile 关键字的使用变得很是重要。
在当前的Java内存模型下, 线程能够把变量保存在本地内存(好比机器的寄存器)中,而不是直接在主存中进行读写。这就可能形成一个线程在主存中修改了一个变量的值,而另一个线程还继续使用它在 寄存器中的变量值的拷贝,形成数据的不一致。
要解决这个问题,只须要像在本程序中的这样,把该变量声明为volatile(不稳定的)便可,这就指示JJVM,这个变量是不稳定的,每次使用它都到主存中进行读取。通常说来,多任务环境下各任务间共享的标志都应该加volatile修饰。
Volatile修饰的 成员变量在每次被 线程访问时,都强迫从 共享内存中重读该成员变量的值。并且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任什么时候刻,两个不一样的线程老是看到某个成员变量的同一个值。
Java语言规范中指出:为了得到最佳速度,容许线程保存共享成员变量的私有拷贝,并且只当线程进入或者离开 同步代码块时才与共享成员变量的原始值对比。
这样当多个线程同时与某个对象交互时,就必需要注意到要让线程及时的获得共享成员变量的变化。
而volatile 关键字就是提示JVM:对于这个 成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用建议:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为 常量时,没必要使用。
因为使用volatile屏蔽掉了JVM中必要的 代码优化,因此在效率上比较低,所以必定在必要时才使用此 关键字

正确使用

编辑
Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量。这两种机制的提出都是为了实现代码线程的安全性。其中 Volatile变量的同步性较差(但有时它更简单而且开销更低),并且其使用也更容易出错。在这期的 Java 理论与实践中,Brian Goetz 将介绍几种正确使用 volatile变量的模式,并针对其适用性限制提出一些建议。
Java 语言中的 volatile变量能够被看做是一种 “程度较轻的 synchronized”;与 synchronized 块相比,volatile 变量所需的编码较少,而且运行时开销也较少,可是它所能实现的功能也仅是 synchronized 的一部分。本文介绍了几种有效使用 volatile变量的模式,并强调了几种不适合使用 volatile 变量的情形。
锁提供了两种主要特性: 互斥(mutual exclusion)可见性(visibility)。互斥即一次只容许一个 线程持有某个特定的锁,所以可以使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程可以使用该共享数据。可见性要更加复杂一些,它必须确保释放锁以前对共享数据作出的更改对于随后得到该锁的另外一个线程是可见的 —— 若是没有同步机制提供的这种可见性保证,线程看到的共享变量多是修改前的值或不一致的值,这将引起许多严重问题。
Volatile 变量
Volatile变量具备 synchronized 的可见性特性,可是不具有原子特性。这就是说线程可以自动发现 volatile变量的最新值。Volatile变量可用于提供 线程安全,可是只能应用于很是有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。所以,单独使用 volatile 还不足以实现计数器、 互斥锁或任何具备与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile变量而不是锁。当使用 volatile变量而非锁时,某些习惯用法(idiom)更加易于编码和阅读。此外,volatile变量不会像锁那样形成 线程阻塞,所以也不多形成可伸缩性问题。在某些状况下,若是读操做远远大于写操做,volatile变量还能够提供优于锁的性能优点。
正确使用 volatile 变量的条件
您只能在有限的一些情形下使用 volatile变量替代锁。要使 volatile变量提供理想的 线程安全,必须同时知足下面两个条件:
● 对变量的写操做不依赖于当前值。
● 该变量没有包含在具备其余变量的不变式中。
实际上,这些条件代表,能够被写入 volatile变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使 volatile变量不能用做线程安全计数器。虽然增量操做(x++)看上去相似一个单独操做,实际上它是一个由读取-修改-写入操做序列组成的组合操做,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操做须要使 x 的值在操做期间保持不变,而 volatile变量没法实现这点。(然而,若是将值调整为只从单个线程写入,那么能够忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile变量不能像 synchronized 那样广泛适用于实现 线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界老是小于或等于上界。

使用方法

编辑
清单 1. 非 线程安全的数值范围类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@NotThreadSafe
public  class  NumberRange{
     private  int  lower,upper;
     public  int  getLower(){
         return  lower;
     }
     public  int  getUpper(){
         return  upper;
     }
     public  void  setLower( int  value){
         if (value > upper)  throw  new  IllegalArgumentException(...);
         lower = value;
     }
     public  void  setUpper( int  value){
         if (value < lower)  throw  new  IllegalArgumentException(...);
         upper = value;
     }
}
这种方式限制了范围的状态变量,所以将 lower 和 upper 字段定义为 volatile 类型不可以充分实现类的线程安全;从而仍然须要使用同步。不然,若是凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,若是初始状态是 (0,5),同一时间内,线程 A 调用 setLower⑷ 而且线程 B 调用 setUpper⑶,显然这两个操做交叉存入的值是不符合条件的,那么两个线程都会经过用于保护不变式的检查,使得最后的范围值是 (4,3) —— 一个无效值。至于针对范围的其余操做,咱们须要使 setLower() 和 setUpper() 操做原子化 —— 而将字段定义为 volatile 类型是没法实现这一目的的。
性能考虑
使用 volatile变量的主要缘由是其简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得多。使用 volatile变量次要缘由是其性能:某些状况下,volatile 变量同步机制的性能要优于锁。
很难作出准确、全面的评价,例如 “X 老是比 Y 快”,尤为是对 JJVM 内在的操做而言。(例如,某些状况下 JVM 也许可以彻底删除锁机制,这使得咱们难以抽象地比较 volatile和 synchronized 的开销。)就是说,在目前大多数的处理器架构上,volatile 读操做开销很是低 —— 几乎和非 volatile 读操做同样。而 volatile 写操做的开销要比非 volatile 写操做多不少,由于要保证可见性须要实现内存界定(Memory Fence),即使如此,volatile 的总开销仍然要比锁获取低。
volatile 操做不会像锁同样形成阻塞,所以,在可以安全使用 volatile 的状况下,volatile 能够提供一些优于锁的可伸缩特性。若是读操做的次数要远远超过写操做,与锁相比,volatile变量一般可以减小同步的性能开销。
正确使用 volatile 的模式
不少并发性专家事实上每每引导用户远离 volatile变量,由于使用它们要比使用锁更加容易出错。然而,若是谨慎地遵循一些良好定义的模式,就可以在不少场合内安全地使用 volatile 变量。要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其余内容时才能使用 volatile —— 这条规则可以避免将这些模式扩展到不安全的用例。
模式 #1:状态标志也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
不少应用程序包含了一种控制结构,形式为 “在尚未准备好中止程序时再执行一些工做”,如清单 2 所示:
清单 2. 将 volatile变量做为 状态标志使用
1
2
3
4
5
6
7
8
9
10
11
12
13
volatile  boolean shutdownRequested;
...
public  void  shutdown()
{
     shutdownRequested= true ;
}
public  void  doWork()
{
     while (!shutdownRequested)
     {
         //dostuff
     }
}
极可能会从循环外部调用 shutdown() 方法 —— 即在另外一个线程中 —— 所以,须要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。(可能会从 JMX 侦听程序、GUI 事件线程中的操做侦听程序、经过 RMI 、经过一个 Web 服务等调用)。然而,使用 synchronized 块编写循环要比使用清单 2 所示的 volatile 状态标志编写麻烦不少。因为 volatile 简化了编码,而且状态标志并不依赖于程序内任何其余状态,所以此处很是适合使用 volatile。
这种类型的状态标记的一个公共特性是:一般只有一种状态转换;shutdownRequested 标志从 false 转换为 true,而后程序中止。这种模式能够扩展到来回转换的状态标志,可是只有在转换周期不被察觉的状况下才能扩展(从 false 到 true,再转换到 false)。此外,还须要某些原子状态转换机制,例如原子变量。
模式 #2:一次性安全发布(one-time safe publication)
缺少同步会致使没法实现可见性,这使得肯定什么时候写入对象引用而不是 原语值变得更加困难。在缺少同步的状况下,可能会遇到某个对象引用的更新值(由另外一个线程写入)和该对象状态的旧值同时存在。(这就是形成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的状况下进行读操做,产生的问题是您可能会看到一个更新的引用,可是仍然会经过该引用看到不彻底构造的对象)。
实现安全发布对象的一种技术就是将对象引用定义为 volatile 类型。清单 3 展现了一个示例,其中后台线程在启动阶段从数据库加载一些数据。其余代码在可以利用这些数据时,在使用以前将检查这些数据是否曾经发布过。
清单 3. 将 volatile变量用于一次性安全发布
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public  class  BackgroundFloobleLoader{
     public  volatile  Flooble theFlooble;
     public  void  initInBackground(){
         //dolotsofstuff
         theFlooble = newFlooble();
         //this is the only write to theFlooble
     }
}
public  class  SomeOtherClass{
     public  void  doWork(){
         while ( true ){
             //dosomestuff...
             //usetheFlooble,butonlyifitisready
             if (floobleLoader.theFlooble!= null )doSomething(floobleLoader.theFlooble);
         }
     }
}
若是 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会获得一个不彻底构造的 Flooble。
该模式的一个必要条件是:被发布的对象必须是 线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布以后永远不会被修改)。volatile 类型的引用能够确保对象的发布形式的可见性,可是若是对象的状态在发布后将发生更改,那么就须要额外的同步。
模式 #3:独立观察(independent observation)
安全使用 volatile 的另外一种简单模式是:按期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器可以感受环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。而后,其余 线程能够读取这个变量,从而随时可以看到最新的温度值。
使用该模式的另外一种应用程序就是收集程序的统计信息。清单 4 展现了 身份验证机制如何记忆最近一次登陆的用户的名字。将反复使用 lastUser 引用来发布值,以供程序的其余部分使用。
清单 4. 将 volatile变量用于多个独立观察结果的发布
1
2
3
4
5
6
7
8
9
10
11
12
public  class  UserManager{
     public  volatile  String lastUser;
     public  boolean  authenticate(String user, String password){
         boolean  valid = passwordIsValid(user, password);
         if (valid){
             User u =  new  User();
             activeUsers.add(u);
             lastUser = user;
         }
         return  valid;
     }
}
该模式是前面模式的扩展;将某个值发布以在程序内的其余地方使用,可是与一次性事件的发布不一样,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使用该值的代码须要清楚该值可能随时发生变化。
模式 #4:“volatile bean” 模式
volatile bean 模式适用于将 JavaBeans 做为“荣誉结构”使用的框架。在 volatile bean 模式中,JavaBean 被用做一组具备 getter 和/或 setter 方法 的独立属性的容器。volatile bean 模式的基本原理是:不少框架为易变数据的持有者(例如 HttpSession)提供了容器,可是放入这些容器中的对象必须是线程安全的。
在 volatile bean 模式中,JavaBean 的全部 数据成员都是 volatile 类型的,而且 getter 和 setter 方法必须很是普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于 对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具备 数组值的属性,由于当数组引用被声明为 volatile 时,只有引用而不是数组自己具备 volatile 语义)。对于任何 volatile变量,不变式或约束都不能包含 JavaBean 属性。清单 5 中的示例展现了遵照 volatile bean 模式的 JavaBean:
清单 5. 遵照 volatile bean 模式的 Person 对象
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@ThreadSafe
public  class  Person{
     private  volatile  String firstName;
     private  volatile  String lastName;
     private  volatile  intage;
     public  String getFirstName(){
         return  firstName;
     }
     public  String getLastName(){
         return  lastName;
     }
     public  int  getAge(){
         return  age;
     }
     public  void  setFirstName(String firstName){
         this .firstName = firstName;
     }
     public  void  setLastName(String lastName){
         this .lastName = lastName;
     }
     public  void  setAge( int  age){
         this .age = age;
     }
}
volatile 的高级模式
前面几节介绍的模式涵盖了大部分的基本用例,在这些模式中使用 volatile 很是有用而且简单。这一节将介绍一种更加高级的模式,在该模式中,volatile 将提供性能或可伸缩性优点。
volatile 应用的的高级模式很是脆弱。所以,必须对假设的条件仔细证实,而且这些模式被严格地封装了起来,由于即便很是小的更改也会损坏您的代码!一样,使用更高级的 volatile 用例的缘由是它可以提高性能,确保在开始应用高级模式以前,真正肯定须要实现这种性能获益。须要对这些模式进行权衡,放弃可读性或可维护性来换取可能的性能收益 —— 若是您不须要提高性能(或者不可以经过一个严格的 测试程序证实您须要它),那么这极可能是一次糟糕的交易,由于您极可能会得不偿失,换来的东西要比放弃的东西价值更低。
模式 #5:开销较低的读-写锁策略
目前为止,您应该了解了 volatile 的功能还不足以实现计数器。由于 ++x 其实是三种操做(读、添加、存储)的简单组合,若是多个线程凑巧试图同时对 volatile 计数器执行增量操做,那么它的更新值有可能会丢失。
然而,若是读操做远远超过写操做,您能够结合使用内部锁和 volatile变量来减小公共代码路径的开销。清单 6 中显示的 线程安全的计数器使用 synchronized 确保增量操做是原子的,并使用 volatile 保证当前结果的可见性。若是更新不频繁的话,该方法可实现更好的性能,由于读路径的开销仅仅涉及 volatile 读操做,这一般要优于一个无竞争的锁获取的开销。
清单 6. 结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”
1
2
3
4
5
6
7
8
9
10
11
12
13
@ThreadSafe
public  class  CheesyCounter{
     //Employs the cheap read-write lock trick
     //All mutative operations MUST be done with the 'this' lock held
     @GuardedBy ( "this" )
     private  volatile  int  value;
         public  int  getValue(){
         return  value;
     }
     public  synchronized  int  increment(){
         return  value++;
     }
}
之因此将这种技术称之为 “开销较低的读-写锁” 是由于您使用了不一样的同步机制进行读写操做。由于本例中的写操做违反了使用 volatile 的第一个条件,所以不能使用 volatile 安全地实现计数器 —— 您必须使用锁。然而,您能够在读操做中使用 volatile 确保当前值的 可见性,所以可使用锁进行全部变化的操做,使用 volatile 进行只读操做。其中,锁一次只容许一个线程访问值,volatile 容许多个线程执行读操做,所以当使用 volatile 保证读代码路径时,要比使用锁执行所有代码路径得到更高的共享度 —— 就像读-写操做同样。然而,要随时牢记这种模式的弱点:若是超越了该模式的最基本应用,结合这两个竞争的同步机制将变得很是困难。
结束语
与锁相比,Volatile变量是一种很是简单但同时又很是脆弱的同步机制,它在某些状况下将提供优于锁的性能和伸缩性。若是严格遵循 volatile 的使用条件 —— 即变量真正独立于其余变量和本身之前的值 —— 在某些状况下可使用 volatile 代替 synchronized 来简化代码。然而,使用 volatile 的代码每每比使用锁的代码更加容易出错。本文介绍的模式涵盖了可使用 volatile 代替 synchronized 的最多见的一些用例。遵循这些模式(注意使用时不要超过各自的限制)能够帮助您安全地实现大多数用例,使用 volatile变量得到更佳性能。
相关文章
相关标签/搜索