写点什么

JAVA 语法 - 关键字 - volatile

作者:Java高工P7
  • 2021 年 11 月 11 日
  • 本文字数:11929 字

    阅读完需:约 39 分钟

当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致


举一个不太准确的例子:


发薪资时,会计每次都把员工叫来登记他们的银行卡号;一次会计为了省事,没有即时登记,用了以前登记的银行卡号;刚好一个员工的银行卡丢了,已挂失该银行卡号;从而造成该员工领不到工资


员工 -- 原始变量地址


银行卡号 -- 原始变量在寄存器的备份


⒉ 在什么情况下会出现


1). 并行设备的硬件寄存器


2). 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)


3). 多线程应用中被几个任务共享的变量


补充:volatile 应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人;


“易变”是因为外在因素引起的,像多线程,中断等,并不是因为用 volatile 修饰了的变量就是“易变”了,假如没有外因,即使用 volatile 定义,它也不会变化;


而用 volatile 定义之后,其实这个变量就不会因外因而变化了,可以放心使用了; 大家看看前面那种解释(易变的)是不是在误导人


volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。


使用该关键字的例子如下:


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p></td><td><p><code>volatile</code> <code>int</code> <code>vint;</code></p></td></tr></tbody></table>


当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。


例如:


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p></td><td><p><code>volatile</code> <code>int</code> <code>i=10;</code></p><p><code>int</code> <code>a=i;</code></p><p><code>//...</code></p></td></tr></tbody></table>


//其他代码,并未明确告诉编译器,对 i 进行过操作


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p></td><td><p><code>int</code> <code>b=i;</code></p></td></tr></tbody></table>


volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i 的地址中读取,因而编译器生成的汇编代码会重新从 i 的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i 读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样一来,如果 i 是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。


注意,在 vc6 中,一般调试模式没有进行代码优化,所以这个关键字的作用看不出来。下面通过插入汇编代码,测试有无 volatile关键字,对程序最终代码的影响:


首先,用 classwizard 建一个 win32 console 工程,插入一个 voltest.cpp 文件,输入下面的代码:


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p><p>12</p><p>13</p><p>14</p></td><td><p><code>#include<stdio.h></code></p><p><code>voidmain(intargc,</code><code>char</code><code>*argv[])</code></p><p><code>{</code></p><p><code>inti=10;</code></p><p><code>inta=i;</code></p><p><code>printf</code><code>(</code><code>"i=%d"</code><code>,a);</code></p><p><code>//下面汇编语句的作用就是改变内存中 i 的值,但是又不让编译器知道</code></p><p><code>__asm</code></p><p><code>{</code></p><p><code>movdwordptr[ebp-4],20h</code></p><p><code>}</code></p><p><code>intb=i;</code></p><p><code>printf</code><code>(</code><code>"i=%d"</code><code>,b);</code></p><p><code>}</code></p></td></tr></tbody></table>


然后,在调试版本模式运行程序,输出结果如下:


i = 10


i = 32


然后,在 release 版本模式运行程序,输出结果如下:


i = 10


i = 10


输出的结果明显表明,release 模式下,编译器对代码进行了优化,第二次没有输出正确的 i 值。下面,我们把 i 的声明加上 volatile关键字,看看有什么变化:


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p><p>12</p><p>13</p></td><td><p><code>#include<stdio.h></code></p><p><code>voidmain(intargc,</code><code>char</code><code>*argv[])</code></p><p><code>{</code></p><p><code>volatileinti=10;</code></p><p><code>inta=i;</code></p><p><code>printf</code><code>(</code><code>"i=%d"</code><code>,a);</code></p><p><code>__asm</code></p><p><code>{</code></p><p><code>movdwordptr[ebp-4],20h</code></p><p><code>}</code></p><p><code>intb=i;</code></p><p><code>printf</code><code>(</code><code>"i=%d"</code><code>,b);</code></p><p><code>}</code></p></td></tr></tbody></table>


分别在调试版本和 release 版本运行程序,输出都是:


i = 10


i = 32


这说明这个关键字发挥了它的作用!


------------------------------------


volatile 对应的变量可能在你的程序本身不知道的情况下发生改变


比如多线程的程序,共同访问的内存当中,多个程序都可以操纵这个变量


你自己的程序,是无法判定何时这个变量会发生变化


还比如,他和一个外部设备的某个状态对应,当外部设备发生操作的时候,通过驱动程序和中断事件,系统改变了这个变量的数值,而你的程序并不知道。


对于 volatile 类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,而不会利用 cache 当中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化——显然也是因为它的数值随时都可能变化的情况。


--------------------------------------------------------------------------------


典型的例子


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p></td><td><p><code>for</code><code>(inti=0;i<100000;i++);</code></p></td></tr></tbody></table>


这个语句用来测试空循环的速度的


但是编译器肯定要把它优化掉,根本就不执行


如果你写成


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p></td><td><p><code>for</code><code>(volatileinti=0;i<100000;i++);</code></p></td></tr></tbody></table>


它就会执行了


volatile 的本意是“易变的”


由于访问寄存器的速度要快过 RAM,所以编译器一般都会作减少存取外部 RAM 的优化。比如:


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p></td><td><p><code>staticinti=0;</code></p><p><code>intmain(</code><code>void</code><code>)</code></p><p><code>{</code></p><p><code>//...</code></p><p><code>while</code><code>(1)</code></p><p><code>{</code></p><p><code>if</code><code>(i)dosomething();</code></p><p><code>}</code></p><p><code>}</code></p><p><code>/Interruptserviceroutine./</code></p><p><code>voidISR_2(</code><code>void</code><code>){i=1;}</code></p></td></tr></tbody></table>


程序的本意是希望 ISR_2 中断产生时,在 main 当中调用 dosomething 函数,但是,由于编译器判断在main函数里面没有修改过 i,因此


可能只执行一次对从 i 到某寄存器的读操作,然后每次 if 判断都只使用这个寄存器里面的“i 副本”,导致 dosomething 永远也不会被


调用。如果将变量加上 volatile 修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中 i 也应该如此说明。


3 使用地方编辑




一般说来,volatile 用在如下的几个地方:


1、中断服务程序中修改的供其它程序检测的变量需要加 volatile;


2、多任务环境下各任务间共享的标志应该加 volatile;


3、存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能有不同意义;


另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在 1 中可以通过关中断来实现,2 中可以禁止任务调度,3 中则只能依靠硬件的良好设计了。


4 代码编辑




下面我们来一个个说明。


考虑下面的代码:


代码:


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p><p>12</p><p>13</p><p>14</p><p>15</p><p>16</p><p>17</p><p>18</p></td><td><p><code>classGadget</code></p><p><code>{</code></p><p><code>    </code><code>public</code><code>:</code></p><p><code>        </code><code>void</code> <code>Wait()</code></p><p><code>        </code><code>{</code></p><p><code>            </code><code>while</code><code>(!flag_)</code></p><p><code>            </code><code>{</code></p><p><code>                </code><code>Sleep(1000);</code><code>//sleeps for 1000milli seconds</code></p><p><code>            </code><code>}</code></p><p><code>        </code><code>}</code></p><p><code>        </code><code>void</code> <code>Wakeup()</code></p><p><code>        </code><code>{</code></p><p><code>            </code><code>flag_=</code><code>true</code><code>;</code></p><p><code>        </code><code>}</code></p><p><code>        </code><code>//...</code></p><p><code>    </code><code>private</code><code>:</code></p><p><code>        </code><code>bool</code> <code>flag_;</code></p><p><code>};</code></p></td></tr></tbody></table>


上面代码中 Gadget::Wait 的目的是每过一秒钟去检查一下 flag_成员变量,当 flag_被另一个线程设为 true 时,该函数才会返回。至少这是程序作者的意图,然而,这个 Wait 函数是错误的。


假设编译器发现 Sleep(1000)是调用一个外部的库函数,它不会改变成员变量flag_,那么编译器就可以断定它可以把 flag_缓存在寄存器中,以后可以访问该寄存器来代替访问较慢的主板上的内存。这对于单线程代码来说是一个很好的优化,但是在现在这种情况下,它破坏了程序的正确性:当你调用了某个 Gadget 的 Wait 函数后


【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


,即使另一个线程调用了 Wakeup,Wait 还是会一直循环下去。这是因为 flag_的改变没有反映到缓存它的寄存器中去。编译器的优化未免有点太……乐观了。


在大多数情况下,把变量缓存在寄存器中是一个非常有价值的优化方法,如果不用的话很可惜。C 和 C++给你提供了显式禁用这种缓存优化的机会。如果你声明变量是使用了 volatile修饰符编译器就不会把这个变量缓存在寄存器里——每次访问都将去存取变量在内存中的实际位置。这样你要对 Gadget 的 Wait/Wakeup 做的修改就是给 flag_加上正确的修饰:


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p></td><td><p><code>class</code> <code>Gadget</code></p><p><code>{</code></p><p><code>    </code><code>public</code><code>:</code></p><p><code>        </code><code>//...as above...</code></p><p><code>    </code><code>private</code><code>:</code></p><p><code>        </code><code>volatile</code> <code>bool</code> <code>flag_;</code></p><p><code>};</code></p></td></tr></tbody></table>


在 Java 中设置变量值的操作,除了 long 和 double 类型的变量外都是原子操作,也就是说,对于变量值的简单读写操作没有必要进行同步。


这在 JJVM 1.2 之前,Java 的内存模型实现总是从主存读取变量,是不需要进行特别的注意的。而随着 JJVM 的成熟和优化,现在在多线程环境下 volatile关键字的使用变得非常重要。


在当前的 Java 内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。


要解决这个问题,只需要像在本程序中的这样,把该变量声明为 volatile(不稳定的)即可,这就指示 JJVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加 volatile 修饰。


Volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。


Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。


这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。


而 volatile关键字就是提示 JVM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。


使用建议:在两个或者更多的线程访问的成员变量上使用 volatile。当要访问的变量已在 synchronized 代码块中,或者为常量时,不必使用。


由于使用 volatile 屏蔽掉了 JVM 中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字


5 正确使用编辑




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 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。


6 使用方法编辑




清单 1. 非线程安全的数值范围类


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p><p>12</p><p>13</p><p>14</p><p>15</p><p>16</p><p>17</p><p>18</p></td><td><p><code>@NotThreadSafe</code></p><p><code>public</code> <code>class</code> <code>NumberRange{</code></p><p><code>    </code><code>private</code> <code>int</code> <code>lower,upper;</code></p><p><code>    </code><code>public</code> <code>int</code> <code>getLower(){</code></p><p><code>        </code><code>return</code> <code>lower;</code></p><p><code>    </code><code>}</code></p><p><code>    </code><code>public</code> <code>int</code> <code>getUpper(){</code></p><p><code>        </code><code>return</code> <code>upper;</code></p><p><code>    </code><code>}</code></p><p><code>    </code><code>public</code> <code>void</code> <code>setLower(</code><code>int</code> <code>value){</code></p><p><code>        </code><code>if</code><code>(value > upper) </code><code>throw</code> <code>new</code> <code>IllegalArgumentException(...);</code></p><p><code>        </code><code>lower = value;</code></p><p><code>    </code><code>}</code></p><p><code>    </code><code>public</code> <code>void</code> <code>setUpper(</code><code>int</code> <code>value){</code></p><p><code>        </code><code>if</code><code>(value < lower) </code><code>throw</code> <code>new</code> <code>IllegalArgumentException(...);</code></p><p><code>        </code><code>upper = value;</code></p><p><code>    </code><code>}</code></p><p><code>}</code></p></td></tr></tbody></table>


这种方式限制了范围的状态变量,因此将 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变量作为状态标志使用


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p><p>12</p><p>13</p></td><td><p><code>volatile</code> <code>boolean shutdownRequested;</code></p><p><code>...</code></p><p><code>public</code> <code>void</code> <code>shutdown()</code></p><p><code>{</code></p><p><code>    </code><code>shutdownRequested=</code><code>true</code><code>;</code></p><p><code>}</code></p><p><code>public</code> <code>void</code> <code>doWork()</code></p><p><code>{</code></p><p><code>    </code><code>while</code><code>(!shutdownRequested)</code></p><p><code>    </code><code>{</code></p><p><code>        </code><code>//dostuff</code></p><p><code>    </code><code>}</code></p><p><code>}</code></p></td></tr></tbody></table>


很可能会从循环外部调用 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变量用于一次性安全发布


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p><p>12</p><p>13</p><p>14</p><p>15</p><p>16</p><p>17</p></td><td><p><code>public</code> <code>class</code> <code>BackgroundFloobleLoader{</code></p><p><code>    </code><code>public</code> <code>volatile</code> <code>Flooble theFlooble;</code></p><p><code>    </code><code>public</code> <code>void</code> <code>initInBackground(){</code></p><p><code>        </code><code>//dolotsofstuff</code></p><p><code>        </code><code>theFlooble = newFlooble();</code></p><p><code>        </code><code>//this is the only write to theFlooble</code></p><p><code>    </code><code>}</code></p><p><code>}</code></p><p><code>public</code> <code>class</code> <code>SomeOtherClass{</code></p><p><code>    </code><code>public</code> <code>void</code> <code>doWork(){</code></p><p><code>        </code><code>while</code><code>(</code><code>true</code><code>){</code></p><p><code>            </code><code>//dosomestuff...</code></p><p><code>            </code><code>//usetheFlooble,butonlyifitisready</code></p><p><code>           </code><code>if</code><code>(floobleLoader.theFlooble!=</code><code>null</code><code>)doSomething(floobleLoader.theFlooble);</code></p><p><code>        </code><code>}</code></p><p><code>    </code><code>}</code></p><p><code>}</code></p></td></tr></tbody></table>


如果 theFlooble 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会得到一个不完全构造的 Flooble。


该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性,但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。


模式 #3:独立观察(independent observation)


安全使用 volatile 的另一种简单模式是:定期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。


使用该模式的另一种应用程序就是收集程序的统计信息。清单 4 展示了身份验证机制如何记忆最近一次登录的用户的名字。将反复使用 lastUser 引用来发布值,以供程序的其他部分使用。


清单 4. 将 volatile变量用于多个独立观察结果的发布


<table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td><p>1</p><p>2</p><p>3</p><p>4</p><p>5</p><p>6</p><p>7</p><p>8</p><p>9</p><p>10</p><p>11</p><p>12</p></td><td><p><code>publicclassUserManager{</code></p><p><code>publicvolatileStringlastUser;</code></p><p><code>publicbooleanauthenticate(Stringuser,Stringpassword){</code></p><p><code>booleanvalid=passwordIsValid(user,password);</code></p><p><code>if</code><code>(valid){</code></p><p><code>Useru=newUser();</code></p><p><code>activeUsers.add(u);</code></p><p><code>lastUser=user;</code></p><p><code>}</code></p><p><code>returnvalid;</code></p><p><code>}</code></p><p><code>}</code></p></td></tr></tbody></table>


该模式是前面模式的扩展;将某个值发布以在程序内的其他地方使用,但是与一次性事件的发布不同,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使用该值的代码需要清楚该值可能随时发生变化。


模式 #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:

用户头像

Java高工P7

关注

还未添加个人签名 2021.11.08 加入

还未添加个人简介

评论

发布
暂无评论
JAVA 语法 - 关键字 - volatile