只有在下列状况时,一个线程对字段的修改才能确保对另外一个线程可见:html
一个写操做线程释放一个锁以后,另外一个读线程随后获取了同一个锁。本质上,线程释放锁时会将强制刷新工做
内存中的数据到主内存中,获取一个锁将强制线程装载(或从新装载)字段的值。锁提供对一个同步方法或块的
互斥性执行,线程执行获取锁和释放锁时,全部对字段的访问的内存效果都是已定义的。
复制代码
注意同步的双重含义:锁提供高级同步协议,同时在线程执行同步方法或块时,内存系统(有时经过内存屏障指令)保证值的一致性。这说明,与顺序程序设计相比较,并发程序设计与分布式程序设计更加相似。同步的第二个特性能够视为一种机制:一个线程在运行已同步方法时,它将发送或接收其余线程在同步方法中对变量所作的修改。从这一点来讲,使用锁和发送消息仅仅是语法不一样而已。java
若是把一个字段声明为volatile型,线程对这个字段值修改后,在执行后续的内存访问以前,线程必须刷新这个字段值且让这个字段值对其余线程可见(即该字段当即刷新)。每次对volatile字段的读访问,都要从新装载字段的值。数据库
一个线程首次访问一个对象的某一个字段,它将读到这个字段的初始值或被某个线程修改后的值。编程
线程终止时,全部写过的变量值都要刷新到主内存中。好比,一个线程使用Thread.join来终止另外一个线程,那么第一个线程确定能看到第二个线程对变量值得修改。数组
注意,在同一个线程的不一样方法之间传递对象的引用,永远也不会出现内存可见性问题。安全
内存模型确保上述操做最终会发生,一个线程对一个特定字段的特定更新,最终将会对其余线程可见,但这个“最终”多是很长一段时间。线程之间没有同步时,很难保证对字段的值能在多线程之间保持一致(指写线程对字段的写入当即能对读线程可见)。特别是,若是字段不是volatile或没有经过同步来访问这个字段,在一个循环中等待其余线程对这个字段的写入,这种状况老是错误的。多线程
从原子性,可见性和有序性的角度分析,声明为volatile字段的做用至关于一个类经过get/set同步方法保护普通字段,以下:架构
final class VFloat {
private float value;
final synchronized void set(float f) { value = f; }
final synchronized float get() { return value; }
}
复制代码
与使用synchronized相比,声明一个volatile字段的区别在于没有涉及到锁操做。但特别的是对volatile字段进行“++”这样的读写操做不会被当作原子操做执行,即volatile不能保证 原! 子! 性!。并发
另外,有序性和可见性仅对volatile字段进行一次读取或更新操做起做用。声明一个引用变量为volatile,不能保证经过该引用变量访问到的非volatile变量的可见性。同理,声明一个数组变量为volatile不能确保数组内元素的可见性。volatile的特性不能在数组内传递,由于数组里的元素不能被声明为volatile。app
因为没有涉及到锁操做,声明volatile字段极可能比使用同步的开销更低,至少不会更高。但若是在方法内频繁访问volatile字段,极可能致使更低的性能,这时还不如锁住整个方法。
若是你不须要锁,把字段声明为volatile是不错的选择,但仍须要确保多线程对该字段的正确访问。可使用volatile的状况包括:
当只有一个线程能够修改字段的值,其它线程能够随时读取,那么把字段声明为volatile是合理的。例如,一个名叫Thermometer的类,能够声明temperature字段为volatile。一个volatile字段很适合做为完成某些工做的标志。再好比,经过使用轻量级的执行框架使某些同步工做自动化,可是仍需把结果字段声明为volatile,使其对各个任务都是可见的。
在前文中已经说起过,线程自己并不直接与主内存进行数据的交互,而是经过线程的工做内存来完成相应的操做。这也是致使线程间数据不可见的本质缘由。所以要实现volatile变量的可见性,直接从这方面入手便可。对volatile变量的写操做与普通变量的主要区别有两点:
(1)修改volatile变量时会强制将修改后的值刷新的主内存中。
(2)修改volatile变量后会致使其余线程工做内存中对应的变量值失效。所以,再读取该变量值的时候就须要从新从读取主内存中的值。
经过这两个操做,就能够解决volatile变量的可见性问题。
在解释这个问题前,咱们先来了解一下Java中的happen-before规则,JSR 133中对Happen-before的定义以下:
Two actions can be ordered by a happens-before relationship.
If one action happens before another,
then the first is visible to and ordered before the second.
复制代码
通俗一点说就是若是a happen-before b,则a所作的任何操做对b是可见的。(这一点你们务必记住,由于happen-before这个词容易被误解为是时间的先后)。咱们再来看看JSR 133中定义了哪些happen-before规则:
翻译过来为:
这里咱们主要看下第三条:volatile变量的保证有序性的规则。《Java并发编程:核心理论》一文中提到太重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会对volatile变量限制这两种类型的重排序。下面是JMM针对volatile变量所规定的重排序规则表:
为了实现volatile可见性和happen-befor的语义。JVM底层是经过一个叫作“内存屏障”的东西来完成。内存屏障,也叫作内存栅栏,是一组处理器指令,用于实现对内存操做的顺序限制。下面是完成上述规则所要求的内存屏障:
(1)LoadLoad 屏障 执行顺序:Load1—>Loadload—>Load2 确保Load2及后续Load指令加载数据以前能访问到Load1加载的数据。
(2)StoreStore 屏障 执行顺序:Store1—>StoreStore—>Store2 确保Store2以及后续Store指令执行前,Store1操做的数据对其它处理器可见。
(3)LoadStore 屏障 执行顺序: Load1—>LoadStore—>Store2 确保Store2和后续Store指令执行前,能够访问到Load1加载的数据。
(4)StoreLoad 屏障 执行顺序: Store1—> StoreLoad—>Load2 确保Load2和后续的Load指令读取以前,Store1的数据对其余处理器是可见的。
下面经过一个实例来讲明一下JVM中是如何插入内存屏障的:
public class MemoryBarrier {
int a, b;
volatile int v, u;
void f() {
int i, j;
i = a;
j = b;
i = v;
//LoadLoad
j = u;
//LoadStore
a = i;
b = j;
//StoreStore
v = i;
//StoreStore
u = j;
//StoreLoad
i = u;
//LoadLoad
//LoadStore
j = b;
a = i;
}
}
复制代码
整体来讲,volatile是并发编程中的一种优化,在某些场景下能够代替Synchronized。可是,volatile的不能彻底取代Synchronized的位置。
使用 volatile 变量的主要缘由是其简易性:在某些情形下,使用 volatile 变量要比使用相应的锁简单得多。使用 volatile 变量次要缘由是其性能:某些状况下,volatile 变量同步机制的性能要优于锁。
在目前大多数的处理器架构上,volatile 读操做开销很是低,几乎和非 volatile 读操做同样。而 volatile 写操做的开销要比非 volatile 写操做多不少,由于要保证可见性须要实现内存界定(Memory Fence),即使如此,volatile 的总开销仍然要比锁获取低。
volatile 操做不会像锁同样形成阻塞,所以,在可以安全使用 volatile 的状况下,volatile 能够提供一些优于锁的可伸缩特性。若是读操做的次数要远远超过写操做,与锁相比,volatile 变量一般可以减小同步的性能开销。
volatile 变量具备 synchronized 的可见性特性,可是不具有原子特性。这就是说线程可以自动发现 volatile 变量的最新值。volatile 变量可用于提供线程安全,可是只能应用于很是有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。所以,单独使用 volatile 还不足以实现计数器、互斥锁或任何具备与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。
出于简易性或可伸缩性的考虑,您可能倾向于使用 volatile 变量而不是锁。当使用 volatile 变量而非锁时,某些习惯用法更加易于编码和阅读。此外,volatile 变量不会像锁那样形成线程阻塞,所以也不多形成可伸缩性问题。在某些状况下,若是读操做远远大于写操做,volatile 变量还能够提供优于锁的性能优点。
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时知足下面两个条件:
实际上,这些条件代表,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。
第一个条件的限制使 volatile 变量不能用做线程安全计数器。虽然增量操做(x++)看上去相似一个单独操做,实际上它是一个由读取-修改-写入操做序列组成的组合操做,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操做须要使 x 的值在操做期间保持不变,而 volatile 变量没法实现这点。(然而,若是将值调整为只从单个线程写入,那么能够忽略第一个条件。)
大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样广泛适用于实现线程安全。下面代码显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界老是小于或等于上界。
@NotThreadSafe
public class NumberRange {
private int lower
private int upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException("lower gt upper");
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException("lower lt upper");
upper = value;
}
}
复制代码
这种方式限制了范围的状态变量,所以将 lower 和 upper 字段定义为 volatile 类型不可以充分实现类的线程安全;从而仍然须要使用同步。不然,若是凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,若是初始状态是 (0, 5),同一时间内,线程 A 调用 setLower(4) 而且线程 B 调用 setUpper(3),显然这两个操做交叉存入的值是不符合条件的,那么两个线程都会经过用于保护不变式的检查,使得最后的范围值是 (4, 3) —— 一个无效值。至于针对范围的其余操做,咱们须要使 setLower() 和 setUpper() 操做原子化 —— 而将字段定义为 volatile 类型是没法实现这一目的的。
不少并发性专家事实上每每引导用户远离 volatile 变量,由于使用它们要比使用锁更加容易出错。然而,若是谨慎地遵循一些良好定义的模式,就可以在不少场合内安全地使用 volatile 变量。要始终牢记使用 volatile 的限制 —— 只有在状态真正独立于程序内其余内容时才能使用 volatile —— 这条规则可以避免将这些模式扩展到不安全的用例。
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
不少应用程序包含了一种控制结构,形式为 “在尚未准备好中止程序时再执行一些工做”,以下所示:
volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }
public void doWork() {
while (!shutdownRequested) {
// do stuff
}
}
复制代码
极可能会从循环外部调用 shutdown() 方法 —— 即在另外一个线程中。 所以,须要执行某种同步来确保正确实现 shutdownRequested 变量的可见性。然而,使用 synchronized 块编写循环要比上面代码 volatile 状态标志编写麻烦不少。因为 volatile 简化了编码,而且状态标志并不依赖于程序内任何其余状态,所以此处很是适合使用 volatile。
这种类型的状态标记的一个公共特性是:一般只有一种状态转换;shutdownRequested 标志从 false 转换为 true,而后程序中止。这种模式能够扩展到来回转换的状态标志,可是只有在转换周期不被察觉的状况下才能扩展(从 false 到 true,再转换到 false)。此外,还须要某些原子状态转换机制,例如原子变量。
缺少同步会致使没法实现可见性,这使得肯定什么时候写入对象引用而不是原语值变得更加困难。在缺少同步的状况下,可能会遇到某个对象引用的更新值(由另外一个线程写入)和该对象状态的旧值同时存在。(这就是形成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引用在没有同步的状况下进行读操做,产生的问题是您可能会看到一个更新的引用,可是仍然会经过该引用看到不彻底构造的对象)。
DCL(double-checked-locking):
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
如今咱们分析一下为何要在变量singleton之间加上volatile关键字。要理解这个问题,
先要了解对象的构造过程,实例化一个对象其实能够分为三个步骤:
(1)分配内存空间。
(2)初始化对象。
(3)将内存空间的地址赋值给对应的引用。
可是因为操做系统能够对指令进行重排序,因此上面的过程也可能会变成以下过程
(1)分配内存空间。
(2)将内存空间的地址赋值给对应的引用。
(3)初始化对象
若是是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而致使不可预料的结果。
所以,为了防止这个过程的重排序,咱们须要将变量设置为volatile类型的变量。
复制代码
实现安全发布对象的一种技术就是将对象引用定义为 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 引用不是 volatile 类型,doWork() 中的代码在解除对 theFlooble 的引用时,将会获得一个不彻底构造的 Flooble。
该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布以后永远不会被修改)。volatile 类型的引用能够确保对象的发布形式的可见性,可是若是对象的状态在发布后将发生更改,那么就须要额外的同步。
安全使用 volatile 的另外一种简单模式是:按期 “发布” 观察结果供程序内部使用。例如,假设有一种环境传感器可以感受环境温度。一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。而后,其余线程能够读取这个变量,从而随时可以看到最新的温度值。
使用该模式的另外一种应用程序就是收集程序的统计信息。下面示例 展现了身份验证机制如何记忆最近一次登陆的用户的名字。将反复使用 lastUser 引用来发布值,以供程序的其余部分使用。
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;
}
}
复制代码
该模式是前面模式的扩展;将某个值发布,在程序内的其余地方使用,可是与一次性事件的发布不一样,这是一系列独立事件。这个模式要求被发布的值是有效不可变的 —— 即值的状态在发布后不会更改。使用该值的代码须要清楚该值可能随时发生变化。
在 volatile bean 模式中,JavaBean 被用做一组具备 getter 和/或 setter 方法 的独立属性的容器。volatile bean 模式的基本原理是:不少框架为易变数据的持有者(例如 HttpSession)提供了容器,可是放入这些容器中的对象必须是线程安全的。
在 volatile bean 模式中,JavaBean 的全部数据成员都是 volatile 类型的,而且 getter 和 setter 方法必须很是普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具备数组值的属性,由于当数组引用被声明为 volatile 时,只有引用而不是数组自己具备 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。下面展现 的示例展现了遵照 volatile bean 模式的 JavaBean:
@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 应用的的高级模式很是脆弱。所以,必须对假设的条件仔细证实,而且这些模式被严格地封装了起来,由于即便很是小的更改也会损坏您的代码!一样,使用更高级的 volatile 用例的缘由是它可以提高性能,确保在开始应用高级模式以前,真正肯定须要实现这种性能获益。须要对这些模式进行权衡,放弃可读性或可维护性来换取可能的性能收益 —— 若是您不须要提高性能(或者不可以经过一个严格的测试程序证实您须要它),那么这极可能是一次糟糕的交易,由于您极可能会得不偿失,换来的东西要比放弃的东西价值更低。
目前为止,您应该了解了 volatile 的功能还不足以实现计数器。由于 ++x 其实是三种操做(读、添加、存储)的简单组合,若是多个线程凑巧试图同时对 volatile 计数器执行增量操做,那么它的更新值有可能会丢失。
然而,若是读操做远远超过写操做,您能够结合使用内部锁和 volatile 变量来减小公共代码路径的开销。下面示例中显示的线程安全的计数器使用 synchronized 确保增量操做是原子的,并使用 volatile 保证当前结果的可见性。若是更新不频繁的话,该方法可实现更好的性能,由于读路径的开销仅仅涉及 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 保证读代码路径时,要比使用锁执行所有代码路径得到更高的共享度 —— 就像读-写操做同样。然而,要随时牢记这种模式的弱点:若是超越了该模式的最基本应用,结合这两个竞争的同步机制将变得很是困难。