使用 volatile 变量的指导原则-译

使用 volatile 变量的指导原则-译

原本想自己总结,但是发现 Brian Goetz 这篇已经将 volatile 变量的使用写得很到位了。所以就认证翻译哈子。原文:Managing volatility
Guidelines for using volatile variables

Java 语言中的 volatile 变量可被认为是简化的同步 (synchronized lite);与同步块相比 (synchronized blocks),使用它们只需较少的编码,并且常常运行时开销更小;但它们能做的事情只是同步 (synchronized) 能做的一个子集。这篇文章呈现了有效使用 volatile 变量的几个模式——和关于何时不使用它们的警告。

提供了两个主要特性:互斥 (mutual exclusion) 和可见性 (visibility)。互斥意味着一个时刻只有一个线程可以持有特定的锁,这个性质可用来实现用于协调对共享数据的访问的协议,为了使一个时刻只有一个线程将使用共享数据。可见性更加微妙,且必须得确保
在释放锁之前对共享数据做出的改变对于随后获取那个锁的另一个线程是可见的——没有同步提供的可见性保证,对于共享变量,线程可能会看见陈旧的 (stale) 或不一致的值,这可能会导致一系列严重问题。

volatile 变量

volatile 变量跟同步 (synchronized) 共有可见性特性,但没有原子性 (atomicity)特性。这意味着线程将会自动看见最新的值,对于 volatile 变量。它们可被用于提供线程安全性 (thread safety),但仅限于非常有限的几种情况:不会在多个变量之间或一个变量和该变量将来的值之间施加约束。因此,只用 volatile 不足以实现计数器 (counter)、互斥体 (mutex) 或具有与多个变量相关的(例如 “start <=end”)不变量的任何类。

你可能喜欢用 volatile 变量来代替锁,因为两个主要的原因:简单性 (simplicity) 和可扩展性 (scalability)。使用 volatile 变量时一些习惯用法更易于编码和阅读。此外,volatile 变量(不像锁)不会导致线程阻塞,因此它们不大可能导致可扩展性问题。在读远远多于写的情形下,相较于锁,volatile 变量也可以提供性能优势。

正确使用 volatile 的条件

仅在有限的几种情形下你才可以用 volatile 变量代替锁。为了让 volatile 变量提供想要的线程安全性,下面的两个条件必须被满足:

  • 对变量的写不能依赖变量的当前值。
  • 变量不能和其他变量一起参与不变量 (invariant)

基本上,这些条件说明:可被写入一个 volatile 变量的那组有效值独立于任何其他的程序状态,包括该变量的当前状态。

第一个条件取消了 volatile 变量被用作线程安全的计数器 (thread-safe counter) 的资格。虽然自增操作 (x++) 可能看起来像一个单一操作 (single operation),但它实际上是一个复合的必须原子地执行的 read-modify-write 操作序列——而 volatile 并不提供计数器所必需的原子性。正确的操作要求在整个操作期间 x 的值保持不变,使用 volatile 变量这点无法做到。(不管怎样,如果你能安排好永远只从单个线程写值给变量,那么你就可以忽略第一个条件。)

大多数编程情形都将要么与第一个,要么与第二个条件发生冲突,使得跟同步相比 volatile 变量是一个不那么常用的实现线程安全性的合适方式。列表 1 展示了一个非线程安全的数字范围类 (number range class)。它包含一个不变量 (invariant)——即下边界总是小于等于上边界。

译注:本文中有 “volatile 变量是实现线程安全性的一种方式” 的类似表达,乍一看不对吧,仔细揣摩哈上下文,发现作者的意思是在满足适合使用 volatile 的条件时使用 volatile 就能提供线程安全性。这一点确实如此。

Listing 1. Non-thread-safe number range class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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;
}
}

因为 range 对象的状态变量以这种方式被约束,所以使 lower 和 upper 成为 volatile 变量不足以使该类变成线程安全的;同步仍被需要。否则,在某个不幸运的时机,以不一致的值为参数分别执行 setLower 和 setUpper 的两个线程会让 range 对象处于不一致的状态。例如,如果初始状态是 (0, 5),且线程 A 调用 setLower(4),同时线程 B 调用 setUpper(3),然后两个操作被穿插错误得恰到好处,两个都能通过保护不变量的检查,最后 range 对象的状态保持为 (4, 3)——一个无效的值。我们需要使 setLower() 和 setUpper() 变成原子的,相对于 range 的其他操作而言——然而使这些字段变成 volatile 并不能为我们做到这点。

性能考虑

使用 volatile 变量最重要的动机是简单性:在一些情形中,使用 volatile 变量确实比使用相应的锁更简单。第二个动机是性能:在一些情形中,相较于锁定,volatile 变量可能是一个表现更好的同步机制。

使像 “X is always faster than Y” 形式的一般表述准确无误极其困难,特别是涉及到内在的 JVM 操作时。(例如,在一些情形下,虚拟机可能能够完全地移除锁定,这使得很难抽象地讨论 volatile 对 synchronized 的相对成本。)也就是说,在大多数当前的处理器架构上,volatile read 是廉价的 —— 几乎跟 nonvolatile read 一样廉价。跟 nonvolatile write 相比,volatile write 就相当昂贵了,因为需要内存屏障 (memory fencing) 来确保可见性,但通常仍比锁的获取便宜。

不像锁定,volatile 操作从不阻塞,因此在可以安全使用 volatile 的情况下相较于锁定它有一些扩展性优势。在读远远多于写的情况下,跟锁定相比,volatile 变量常常能降低同步的执行成本。

正确使用 volatile 的模式

许多并发专家往往会指导用户完全别用使用 volatile 变量,因为跟锁相比它们更难于被正确地使用。不过,存在一些良好定义的模式,如果你细心地跟随它们的指导,就可以将它们用于各种各样的情形。一直要记着那些关于限制 volatile 可以用在哪儿的规则——只对真正独立于程序中其他一切东西的状态使用 volatile——这样做应该会保护你避免尝试将这些模式扩展至危险的领域。

Pattern #1: status flags

Perhaps the canonical use of volatile variables is simple boolean status flags, indicating that an important one-time life-cycle event has happened, such as initialization has completed or shutdown has been requested.

Many applications include a control construct of the form, “While we’re not ready to shut down, do more work,” as shown in Listing 2:

Listing 2. Using a volatile variable as a status flag

1
2
3
4
5
6
7
8
9
10
11
volatile boolean shutdownRequested;

...

public void shutdown() { shutdownRequested = true; }

public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}

It is likely that the shutdown() method is going to be called from somewhere outside the loop – in another thread – and as such, some form of synchronization is required to ensure the proper visibility of the shutdownRequested variable. (It might be called from a JMX listener, an action listener in the GUI event thread, through RMI, through a Web service, and so on.) However, coding the loop with synchronized blocks would be much more cumbersome than coding it with a volatile status flag as in Listing 2. Because volatile simplifies the coding, and the status flag does not depend on any other state in the program, this is a good use for volatile.

cumbersome: slow and complicated 缓慢复杂的
a transition cycle: 一次变迁轮回/循环
undetected: not noticed by anyone 未被注意的

One common characteristic of status flags of this type is that there is typically only one state transition; the shutdownRequested flag goes from false to true and then the program shuts down. This pattern can be extended to state flags that can change back and forth, but only if it is acceptable for a transition cycle (from false to true to false) to go undetected. Otherwise, some sort of atomic state transition mechanism is needed, such as atomic variables.

[注解] 这个模式的关键特征:

  • 当重要的一次性生命周期事件发生时,表示某个操作(例如 initialization)已完成或者被请求执行某个操作(例如 shutdown)。
  • 对状态标志的检查在一个线程中,对它的原子性写入(即写入不依赖于程序中的任何其他状态,包括其自身的当前状态)在另一个线程中

Pattern #2: one-time safe publication

The visibility failures that are possible in the absence of synchronization can get even trickier to reason about when writing to object references instead of primitive values. In the absence of synchronization, it is possible to see an up-to-date value for an object reference that was written by another thread and still see stale values for that object’s state. (This hazard is the root of the problem with the infamous double-checked-locking idiom, where an object reference is read without synchronization, and the risk is that you could see an up-to-date reference but still observe a partially constructed object through that reference.)

[注解] 前面这段讲述了在没有同步的情况下对对象引用 (object reference) 进行写操作时存在的隐患:虽然看到了引用变量的最新值,但仍然通过那个引用观察到了一个还未完全构建的对象,进而看到不一致的对象内部状态,例如,相关的多个字段还未能满足某种业务约束。

One technique for safely publishing an object is to make the object reference volatile. Listing 3 shows an example where during startup, a background thread loads some data from a database. Other code, when it might be able to make use of this data, checks to see if it has been published before trying to use it.

Listing 3. Using a volatile variable for safe one-time publication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;

public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}

public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}

Without the theFlooble reference being volatile, the code in doWork() would be at risk for seeing a partially constructed Flooble as it dereferences the theFlooble reference.

A key requirement for this pattern is that the object being published must either be thread-safe or effectively immutable (effectively immutable means that its state is never modified after its publication). The volatile reference may guarantee the visibility of the object in its as-published form, but if the state of the object is going to change after publication, then additional synchronization is required.

[注解] 这个模式的关键特征:
要发布的对象必须要么是线程安全的,要么是真正不可变的(这意味着在发布后它的状态绝不会被修改),正如该模式的名字 “one-time safe publication” 所描述的那样。否则,就需要额外的同步。

Pattern #3: independent observations

Another simple pattern for safely using volatile is when observations are periodically “published” for consumption within the program. For example, say there is an environmental sensor that senses the current temperature. A background thread might read this sensor every few seconds and update a volatile variable containing the current temperature. Then, other threads can read this variable knowing that they will always see the most up-to-date value.

Another application for this pattern is gathering statistics about the program. Listing 4 shows how an authentication mechanism might remember the name of the last user to have logged on. The lastUser reference will be repeatedly used to publish a value for consumption by the rest of the program.

Listing 4. Using a volatile variable for multiple publications of independent observations

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}
}

This pattern is an extension of the previous one; a value is being published for use elsewhere within the program, but instead of publication being a one-time event, it is a series of independent events. This pattern requires that the value being published be effectively immutable – that its state not change after publication. Code consuming the value should be aware that it might change at any time.

[注解] 这个模式的关键特征:
独立的观察者持续观察发生的事件并发布相应的值,消费方应该意识到值的引用随时可能指向新的值。这个模式要求正被发布的值必须是真正不可变的。

Pattern #4: the “volatile bean” pattern

The volatile bean pattern is applicable in frameworks that use JavaBeans as “glorified structs.” In the volatile bean pattern, a JavaBean is used as a container for a group of independent properties with getters and/or setters. The rationale for the volatile bean pattern is that many frameworks provide containers for mutable data holders (for instance, HttpSession), but the objects placed in those containers must be thread safe.

glorified adj. making sb/sth seem more important or better than they are 吹捧的;吹嘘的;美化的
rationale n. the principles or reasons which explain a particular decision, course of action, belief, etc. 基本原则/理,基本依据;根本原因
as with

In the volatile bean pattern, all the data members of the JavaBean are volatile, and the getters and setters must be trivial – they must contain no logic other than getting or setting the appropriate property. Further, for data members that are object references, the referred-to objects must be effectively immutable. (This prohibits having array-valued properties, as when an array reference is declared volatile, only the reference, not the elements themselves, have volatile semantics.) As with any volatile variable, there may be no invariants or constraints involving the properties of the JavaBean. An example of a JavaBean obeying the volatile bean pattern is shown in Listing 5:

Listing 5. A Person object obeying the volatile bean pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;

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;
}
}

[注解] 这个模式的关键特征:

  1. 没有任何不变量或约束涉及 JavaBean 的属性,这个 Java Bean 只是一组彼此独立的属性的容器而已。
  2. getter 和 setter 中除了 get 和 set 对应的属性外,没有其他逻辑。
  3. 如果数据成员是对象引用,那么被引用的对象必须是真正不可变的。

满足这三点就可以应用此模式,将其所有数据成员指定为 volatile 的。

Advanced patterns for volatile

The patterns in the previous section cover most of the basic cases where the use of volatile is sensible and straightforward. This section looks at a more advanced pattern where volatile might offer a performance or scalability benefit.

The more advanced patterns for using volatile can be extremely fragile. It is critical that your assumptions be carefully documented and these patterns strongly encapsulated because very small changes can break your code! Also, given that the primary motivation for the more advanced volatile use cases is performance, be sure that you actually have a demonstrated need for the purported performance gain before you start applying them. These patterns are trade-offs that give up readability or maintainability in exchange for a possible performance boost – if you don’t need the performance boost (or can’t prove you need it through a rigorous measurement program), then it is probably a bad trade because you’re giving up something of value and getting something of lesser value in return.

Pattern #5: The cheap read-write lock trick

By now, it should be well-known that volatile is not strong enough to implement a counter. Because ++x is really shorthand for three operations (read, add, store), with some unlucky timing it is possible for updates to be lost if multiple threads tried to increment a volatile counter at once.

However, if reads greatly outnumber modifications, you can combine intrinsic locking and volatile variables to reduce the cost on the common code path. Listing 6 shows a thread-safe counter that uses synchronized to ensure that the increment operation is atomic and uses volatile to guarantee the visibility of the current result. If updates are infrequent, this approach may perform better as the overhead on the read path is only a volatile read, which is generally cheaper than an uncontended lock acquisition.

Listing 6. Combining volatile and synchronized to form a “cheap read-write lock”

1
2
3
4
5
6
7
8
9
10
11
12
@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++;
}
}

The reason this technique is called the “cheap read-write lock” is that you are using different synchronization mechanisms for reads and writes. Because the writes in this case violate the first condition for using volatile, you cannot use volatile to safely implement the counter – you must use locking. However, you can use volatile to ensure the visibility of the current value when reading, so you use locking for all mutative operations and volatile for read-only operations. Where locks only allow one thread to access a value at once, volatile reads allow more than one, so when you use volatile to guard the read code path, you get a higher degree of sharing than you would were you to use locking for all code paths – just like a read-write lock. However, bear in mind the fragility of this pattern: With two competing synchronization mechanisms, this can get very tricky if you branch out beyond the most basic application of this pattern.

[注解] 这个模式的关键特征:

  1. 读远远多于写,并且写操作不是单一的原子操作(例如,++x),需要显示同步
  2. 使用 volatile 来保证读取时能够读到最新的值
  3. 使用锁来保证写操作的线程安全性

通过组合使用 volatile 和锁来模拟 read-write 锁,以获得标榜的性能增长。

Summary

Volatile variables are a simpler – but weaker – form of synchronization than locking, which in some cases offers better performance or scalability than intrinsic locking. If you follow the conditions for using volatile safely – that the variable is truly independent of both other variables and its own prior values – you can sometimes simplify code by using volatile instead of synchronized. However, code using volatile is often more fragile than code using locking. The patterns offered here cover the most common cases where volatile is a sensible alternative to synchronized. Following these patterns – taking care not to push them beyond their limits – should help you safely cover the majority of cases where volatile variables are a win.

Downloadable resources

PDF of this content

Java Concurrency in Practice: The how-to manual for developing concurrent programs in Java code, including constructing and composing thread-safe classes and programs, avoiding liveness hazards, managing performance, and testing concurrent applications.
Going Atomic: Describes the atomic variable classes added in Java 5.0, which extend the concept of volatile variables to support atomic state transitions.
An introduction to nonblocking algorithms: Describes how concurrent algorithms can be implemented without locks, using atomic variables.
Volatiles): More about volatile variables from Wikipedia.

0%