Java语言中的易变变量可以被认为是“ synchronized
精简版”。 与synchronized
块相比,它们需要较少的编码来使用,并且通常具有较少的运行时开销,但是它们只能用于完成synchronized
可以完成的事情的子集。 本文介绍了一些有效使用可变变量的模式-以及有关何时不使用它们的警告。
锁具有两个主要功能: 互斥和可见性 。 互斥意味着一次只能有一个线程持有给定的锁,并且此属性可用于实现协议以协调对共享数据的访问,以便一次仅一个线程将使用共享数据。 可见性更加微妙,这与确保释放锁之前对共享数据所做的更改对随后获得该锁的另一个线程可见-在没有同步提供可见性保证的情况下,线程可能会看到过时的或不一致的值共享变量,这可能会导致许多严重问题。
易变变量
volatile变量共享的可视性功能synchronized
,但没有一个原子的特性。 这意味着线程将自动查看volatile变量的最新值。 它们可用于提供线程安全性,但仅在非常有限的一组情况下使用:它们在多个变量之间或变量的当前值与其将来值之间不施加约束。 因此,仅靠volatile不足以实现计数器,互斥体或任何具有不变量与多个变量相关联的类(例如“ start <= end”)。
出于两个主要原因之一,您可能更喜欢使用易失性变量而不是锁:简单性或可伸缩性。 当某些惯用语使用易失性变量而不是锁时,它们更易于编码和阅读。 此外,易失性变量(与锁不同)不会导致线程阻塞,因此它们不太可能引起可伸缩性问题。 在读取次数大大超过写入次数的情况下,易失性变量也可能提供优于锁定的性能优势。
正确使用挥发物的条件
仅在少数情况下,才可以使用易失性变量而不是锁。 对于可变变量,必须满足以下两个条件才能提供所需的线程安全性:
- 写入变量不取决于其当前值。
- 该变量不与其他变量一起参与不变式。
基本上,这些条件表明可以写入易失性变量的有效值集独立于任何其他程序状态,包括变量的当前状态。
第一个条件使易失变量不能用作线程安全计数器。 虽然增量操作( x++
)看起来像是单个操作,但实际上它是必须以原子方式执行的复合的读-修改-写操作序列,而volatile不提供必要的原子性。 正确的运算将要求x
的值在运算期间保持不变,而这是使用易失性变量无法实现的。 (但是,如果可以安排仅从单个线程写入该值,则可以忽略第一个条件。)
大多数编程情况都会与第一个条件或第二个条件发生冲突,这使得易失性变量成为实现线程安全的一种较不常用的方法,而不是synchronized
。 清单1显示了一个非线程安全的数字范围类。 它包含一个不变式-下限始终小于或等于上限。
清单1.非线程安全的数字范围类
@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
和上字段挥发性不足以使类线程安全; 仍然需要同步。 否则,由于某些不幸的时机,两个执行带有不一致值的setLower
和setUpper
线程可能会使范围处于不一致状态。 例如,如果初始状态为(0, 5)
setLower(4)
(0, 5)
,并且线程A调用线程B调用setUpper(3)
的同时调用setUpper(3)
,并且操作交织在一起只是错误的,那么两者都可以通过应该保护不变量并以范围保持(4, 3)
-无效值。 我们需要使setLower()
和setUpper()
操作相对于该范围上的其他操作是原子的,并且使字段易变对我们来说是无法做到的。
性能考量
使用volatile变量的主要动机是简单:在某些情况下,使用volatile变量比使用相应的锁定更简单。 使用易失变量的次要动机是性能:在某些情况下,易失变量可能是比锁定更好的同步机制。
很难以“ X总是比Y快”的形式做出准确的,通用的语句,特别是涉及到固有的JVM操作时。 (例如,VM在某些情况下可能能够完全删除锁定,这使得很难谈论volatile
与抽象synchronized
的相对成本。)也就是说,在大多数当前处理器架构上,volatile读取都很便宜-几乎与非易失性读取一样便宜。 易失性写比非易失性写要贵得多,这是因为需要保证可见性的内存围栏,但通常比锁获取便宜。
与锁定不同,易失性操作永远不会阻塞,因此在可以安全使用它们的情况下,易失性比锁定提供了一些可伸缩性优势。 在读取数量大大超过写入数量的情况下,与锁定相比,易失性变量通常会降低同步的性能成本。
正确使用挥发物的模式
许多并发专家倾向于引导用户完全不使用可变变量,因为它们比锁更难正确使用。 但是,存在一些定义明确的模式,如果仔细遵循它们,则可以在各种情况下安全地使用它们。 始终牢记有关可使用volatile的限制的规则-仅将volatile用于真正独立于程序中其他所有内容的状态-这样做应避免您尝试将这些模式扩展到危险的领域。
模式1:状态标志
可变变量的规范使用可能是简单的布尔状态标志,表明发生了重要的一次性生命周期事件,例如初始化已完成或已请求关闭。
许多应用程序包括以下形式的控件构造:“虽然我们还没有准备好关闭,但需要做更多的工作”,如清单2所示:
清单2.使用volatile变量作为状态标志
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
很有可能将在循环之外的某个位置(在另一个线程中)调用shutdown()
方法,因此,需要某种形式的同步以确保shutdownRequested
变量的正确可见性。 (可以从JMX侦听器,GUI事件线程中的动作侦听器,通过RMI,通过Web服务等调用它。)但是,用synchronized
块编码循环比用a编码更麻烦。 volatile状态标志,如清单2所示。由于volatile简化了编码,并且status标志不依赖于程序中的任何其他状态,因此这对于volatile是一个很好的用法。
这种状态标志的一个共同特征是通常只有一个状态转换。 shutdownRequested
标志从false
变为true
,然后程序关闭。 可以将这种模式扩展到可以来回更改的状态标志,但前提是必须检测到过渡周期(从false
到true
到false
)不被检测到是可接受的。 否则,需要某种原子状态转换机制,例如原子变量。
模式2:一次性安全发布
在没有同步的情况下可能发生的可见性故障甚至更难于推断何时写入对象引用而不是原始值。 在没有同步的情况下,可以看到由另一个线程编写的对象引用的最新值,并且仍然可以看到该对象状态的陈旧值。 (这种危险是臭名昭著的双重检查锁定习惯用法问题的根源,在这种习俗中,对象引用是在不同步的情况下读取的,其风险是您可以看到最新的引用,但仍然观察到部分构造的对象通过该参考文献。)
一种安全发布对象的技术是使对象引用易变。 清单3显示了一个示例,其中在启动期间,后台线程从数据库中加载了一些数据。 当其他代码可能能够使用此数据时,请在尝试使用它之前检查是否已发布。
清单3.使用volatile变量进行安全的一次性发布
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);
}
}
}
如果没有使theFlooble
引用不稳定,则doWork()
的代码将因为取消引用theFlooble
引用而theFlooble
部分构造的Flooble
的theFlooble
。
此模式的关键要求是,发布的对象必须是线程安全的或有效地不可更改的(有效地不可更改意味着其状态在其发布后永远不会被修改)。 易失性引用可以保证其发布时对象的可见性,但是如果对象的状态在发布后要更改,则需要附加的同步。
模式3:独立观察
安全使用volatile的另一种简单模式是定期“发布”观察结果以供程序使用。 例如,假设有一个环境传感器可以感应当前温度。 后台线程可能每隔几秒钟读取一次此传感器,并更新包含当前温度的易失性变量。 然后,其他线程可以知道该变量始终会看到最新的值来读取该变量。
此模式的另一个应用程序是收集有关程序的统计信息。 清单4显示了身份验证机制如何记住上次登录的用户的名称。 lastUser
引用将被重复用于发布一个值,以供程序的其余部分使用。
清单4.将volatile变量用于独立观察的多个出版物
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模式适用于将JavaBeans用作“标准化结构”的框架。 在易失性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对象
@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;
}
}
易失性的高级模式
上一节中的模式涵盖了使用volatile明智而直接的大多数基本情况。 本节着眼于更高级的模式,其中volatile可能会提供性能或可伸缩性优势。
使用volatile的更高级模式可能非常脆弱。 认真记录您的假设并严格封装这些模式至关重要,因为很小的更改可能会破坏您的代码! 同样,鉴于更高级的易失性用例的主要动机是性能,因此在开始应用它们之前,请确保您确实对证明的性能有所需求。 这些模式是折衷方案,它们放弃了可读性或可维护性,以换取可能的性能提升-如果您不需要性能提升(或无法通过严格的测量程序证明您需要它),则可能是不好的交易,因为您放弃了某些价值而获得了较小价值的回报。
模式5:便宜的读写锁定技巧
到现在为止,众所周知,volatile不足以实现计数器。 由于++x
实际上是三个操作(读取,添加,存储)的简写形式,因此如果运气不好的话,如果多个线程试图一次增加易失性计数器的数量,更新可能会丢失。
但是,如果读取的内容大大超过修改的内容,则可以结合使用内在锁定和易失性变量来减少公共代码路径上的开销。 清单6显示了一个线程安全计数器,该计数器使用synchronized
来确保增量操作是原子的,并使用volatile
来保证当前结果的可见性。 如果不经常更新,则此方法可能会更好,因为读取路径上的开销只是易失性读取,通常比无竞争的锁获取便宜。
清单6.组合volatile和同步以形成“便宜的读写锁”
@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
,而不是synchronized
。 但是,使用volatile
代码通常比使用锁定的代码更脆弱。 这里提供的模式涵盖了最常见的情况,在这些情况下, volatile
是synchronized
的明智选择。 遵循这些模式-注意不要将其推到极限之外-应该可以帮助您安全地涵盖大多数易变变量都是成功的情况。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp06197/index.html