欲加入创客群请加微信:cool-smiler ,备注:入群

深度理解Volatile 变量的使用场景

今日限免 JackLeon 6个月前 (05-08) 236次浏览 0个评论 扫描二维码

深度理解Volatile 变量的使用场景

深度理解 Volatile 变量的使用场景

Volatile 变量解惑

很多并发性专家事实上往往引导⽤户远离 volatile 变量,因为使⽤它们要⽐使⽤锁更加容易出错。然⽽,

如果谨慎地遵循⼀些良好定义的模式,就能够在很多场合内安全地使⽤ volatile 变量。要始终牢记使⽤

volatile 的限制 —— 只有在状态真正独⽴于程序内其他内容时才能使⽤ volatile —— 这条规则能够避免

将这些模式扩展到不安全的⽤例。

Volatile 变量具有 synchronized 的可⻅性特性,但是不具备原⼦特性。这就是说线程能够⾃动发现

volatile 变量的最新值。Volatile 变量可⽤于提供线程安全,但是只能应⽤于⾮常有限的⼀组⽤例:多个

变量之间或者某个变量的当前值与修改后值之间没有约束。因此,单独使⽤ volatile 还不⾜以实现计数

器、互斥锁或任何具有与多个变量相关的不变式(Invariants)的类(例如 “start <=end”)。

出于简易性或可伸缩性的考虑,您可能倾向于使⽤ volatile 变量⽽不是锁。当使⽤ volatile 变量⽽⾮锁

时,某些习惯⽤法(idiom)更加易于编码和阅读。此外,volatile 变量不会像锁那样造成线程阻塞,因此

也很少造成可伸缩性问题。在某些情况下,如果读操作远远⼤于写操作,volatile 变量还可以提供优于锁

的性能优势。

volatile 变量的使用条件

要使 volatile 变量提供理想的线程安全,必须同时满⾜下⾯两个条件:

1.对变量的写操作不依赖于当前值。

2.该变量没有包含在具有其他变量的不变式中。

⾮线程安全的数值范围类案例

@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 类型不能够充分实现类

的线程安全;从⽽仍然需要使⽤同步。否则,如果凑巧两个线程在同⼀时间使⽤不⼀致的值执⾏ setLow

er 和 setUpper 的话,则会使范围处于不⼀致的状态。例如,如果初始状态是 (0, 5) ,同⼀时间

内,线程 A 调⽤ setLower(4) 并且线程 B 调⽤ setUpper(3) ,显然这两个操作交叉存⼊的值是不

符合条件的,那么两个线程都会通过⽤于保护不变式的检查,使得最后的范围值是 (4, 3) —— ⼀个⽆

效值。⾄于针对范围的其他操作,我们需要使 setLower() 和 setUpper() 操作原⼦化 —— ⽽将字

段定义为 volatile 类型是⽆法实现这⼀⽬的的。

volatile 变量性能分析

使⽤ volatile 变量的主要原因是其简易性:在某些情形下,使⽤ volatile 变量要⽐使⽤相应的锁简单得

多。使⽤ volatile 变量次要原因是其性能:某些情况下,volatile 变量同步机制的性能要优于锁。

很难做出准确、全⾯的评价,例如 “X 总是⽐ Y 快”,尤其是对 JVM 内在的操作⽽⾔。(例如,某些情

况下 VM 也许能够完全删除锁机制,这使得我们难以抽象地⽐较 volatile 和 synchronized 的开销。)就是说,在⽬前⼤多数的处理器架构上,volatile 读操作开销⾮常低 —— ⼏乎和⾮ volatile 读操

作⼀样。⽽ volatile 写操作的开销要⽐⾮ volatile 写操作多很多,因为要保证可⻅性需要实现内存界定

(Memory Fence),即便如此,volatile 的总开销仍然要⽐锁获取低。

volatile 操作不会像锁⼀样造成阻塞,因此,在能够安全使⽤ volatile 的情况下,volatile 可以提供⼀些

优于锁的可伸缩特性。如果读操作的次数要远远超过写操作,与锁相⽐,volatile 变量通常能够减少同步

的性能开销。

volatile 变量的使用场景

基本模式使用场景

使用场景一、状态标志

volatile boolean shutdownRequested;

 ...

 public void shutdown() { shutdownRequested = true; }

 public void doWork() {

    while (!shutdownRequested) {

        // do stuff

    }

}

很可能会从循环外部调⽤ shutdown() ⽅法 —— 即在另⼀个线程中 —— 因此,需要执⾏某种同步来确

保正确实现 shutdownRequested 变量的可⻅性。(可能会从 JMX 侦听程序、GUI 事件线程中的操作

侦听程序、通过 RMI 、通过⼀个 Web 服务等调⽤)。然⽽,使⽤ synchronized 块编写循环要⽐使

⽤清单 2 所示的 volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于

程序内任何其他状态,因此此处⾮常适合使⽤ volatile。

这种类型的状态标记的⼀个公共特性是:通常只有⼀种状态转换; shutdownRequested 标志从 fals

e 转换为 true ,然后程序停⽌。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期不被察

觉的情况下才能扩展(从 false 到 true ,再转换到 false )。此外,还需要某些原⼦状态转换机

制,例如原⼦变量。

使用场景二、⼀次性安全发布(one-time safe publication)

缺乏同步会导致⽆法实现可⻅性,这使得确定何时写⼊对象引⽤⽽不是原语值变得更加困难。在缺乏同步

的情况下,可能会遇到某个对象引⽤的更新值(由另⼀个线程写⼊)和该对象状态的旧值同时存在。(这

就是造成著名的双重检查锁定(double-checked-locking)问题的根源,其中对象引⽤在没有同步的情

况下进⾏读操作,产⽣的问题是您可能会看到⼀个更新的引⽤,但是仍然会通过该引⽤看到不完全构造的

对象)。

实现安全发布对象的⼀种技术就是将对象引⽤定义为 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 类型的引⽤可以确保对象的发布形式的可⻅性,但

是如果对象的状态在发布后将发⽣更改,那么就需要额外的同步。

使用场景三、独⽴观察(independent observation)

安全使⽤ 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” 模式

volatile bean 模式适⽤于将 JavaBeans 作为“荣誉结构”使⽤的框架。在 volatile bean 模式中,

JavaBean 被⽤作⼀组具有 getter 和/或 setter ⽅法 的独⽴属性的容器。volatile bean 模式的基本原

理是:很多框架为易变数据的持有者(例如 HttpSession )提供了容器,但是放⼊这些容器中的对象必

须是线程安全的。

在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter ⽅

法必须⾮常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引⽤的数据成

员,引⽤的对象必须是有效不可变的。(这将禁⽌具有数组值的属性,因为当数组引⽤被声明为 volati

le 时,只有引⽤⽽不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包

含 JavaBean 属性。

下面清单 中的示例展示了遵守 volatile bean 模式的 JavaBean:

@ThreadSafe
@Data
public class Person {

    private volatile String firstName;

    private volatile String lastName;

    private volatile int age;
}

高级模式使用场景

前⾯⼏种 volatile 变量的使用场景介绍的模式涵盖了⼤部分的基本⽤例,在这些模式中使⽤ volatile ⾮常有⽤并且简单。接下来将介绍⼀种更加⾼级的模式,在该模式中,volatile 将提供性能或可伸缩性优势。

volatile 应⽤的的⾼级模式⾮常脆弱。因此,必须对假设的条件仔细证明,并且这些模式被严格地封装了

起来,因为即使⾮常⼩的更改也会损坏您的代码!同样,使⽤更⾼级的 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 保证读代码路径时,要⽐使⽤锁执⾏全部代码路径获得更⾼的共享度 —— 就像

读-写操作⼀样。然⽽,要随时牢记这种模式的弱点:如果超越了该模式的最基本应⽤,结合这两个竞争

的同步机制将变得⾮常困难。

总结

与锁相⽐,Volatile 变量是⼀种⾮常简单但同时⼜⾮常脆弱的同步机制,它在某些情况下将提供优于锁的

性能和伸缩性。如果严格遵循 volatile 的使⽤条件 —— 即变量真正独⽴于其他变量和⾃⼰以前的值 ——

在某些情况下可以使⽤ volatile 代替 synchronized 来简化代码。然⽽,使⽤ volatile 的代

码往往⽐使⽤锁的代码更加容易出错。本⽂介绍的模式涵盖了可以使⽤ volatile 代替 synchroniz

ed 的最常⻅的⼀些⽤例。遵循这些模式(注意使⽤时不要超过各⾃的限制)可以帮助您安全地实现⼤多

数⽤例,使⽤ volatile 变量获得更佳性能。





温馨提示:若在升级会员或付费后阅读过程中遇到问题,请加客服微信号(cool-smiler)沟通解决,祝您生活愉快。
转载请注明原文链接:深度理解Volatile 变量的使用场景
喜欢 (0)
[1186664388@qq.com]
分享 (0)
关于作者:
创享视界(creativeview.cn)是一个带动全民颠覆八小时工作制,通过投稿把自己的创意智慧变现的方式创造被动收入,从而实现财务自由的平台。我们相信,创新思维不仅有助于打造更出色的产品,还可以让世界变得更美好,让人人受益。
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
%d 博主赞过: