volatile关键字有什么用处?

805
今天在工作中,我遇到了Java中的volatile关键字。由于不是很熟悉它,我找到了这篇说明文章
鉴于该文章详细解释了关键字的相关内容,您是否曾经使用过它,或者是否可以想象出可以正确使用该关键字的情况?
25个回答

864

volatile 具有内存可见性的语义。基本上,volatile 字段的值在写操作完成后对所有读取者(尤其是其他线程)变得可见。没有 volatile,读取者可能会看到一些未更新的值。

回答你的问题:是的,我使用一个 volatile 变量来控制某些代码是否继续循环。循环测试 volatile 值,如果它为 true,则继续。可以通过调用“停止”方法将条件设置为 false。当循环在执行停止方法后测试该值时,它会看到 false 并终止。

我强烈推荐的书籍 "Java Concurrency in Practice" 对 volatile 进行了很好的解释。这本书是由写了 IBM 文章的同一人编写的(实际上,在那篇文章底部引用了他的书)。我的使用 volatile 是他的文章所称的“模式 1 状态标志”。

如果您想更深入地了解volatile是如何工作的,可以查阅Java内存模型。如果您想超越这个水平,请查看好的计算机体系结构书籍,比如Hennessy&Patterson并阅读关于高速缓存一致性和高速缓存一致性的内容。


159
这个答案是正确的,但不完整。它遗漏了一个重要的 volatile 特性,这个特性随着 JSR 133 中定义的新 Java 内存模型而来:当一个线程读取一个 volatile 变量时,它看到的不仅仅是一些其他线程上最后写入的值,还包括在那个写入 volatile 的时间点上可见的其他变量的所有写操作。请参考 这个答案这个参考资料 - Adam Zalcman
56
初学者,请你用一些代码来演示(可以吗?) - Hungry Blue Dev
8
题目中链接的文章包含代码示例。 - Greg Mattes
@GregMattes,从https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html可以看出,如果值是引用字段并且是“bool”,则似乎不需要使用volatile。因此,在您提供的示例中甚至可能不需要“volatile”。 - jordan
2
@fefrei:“立即”是一个口语化的术语。当执行时间和线程调度算法都没有被明确指定时,这是无法保证的。程序唯一能够找出一个易失性读取是否紧随着一个特定的易失性写入的方法,就是检查所看到的值是否是预期写入的值。 - Holger
显示剩余5条评论

220

“...volatile修饰符保证读取字段的任何线程都将看到最近写入的值。” - Josh Bloch

如果您考虑使用volatile,请阅读处理原子行为的java.util.concurrent包。

维基百科关于单例模式的懒加载文章中展示了volatile的使用。


23
为什么同时存在volatilesynchronized关键字? - ptkato
7
自从那时起,单例模式的维基百科文章已经发生了很大变化,不再包括那个使用volatile关键字的示例。但是,你可以在存档版本中找到它。 - bskp
4
这两个关键词的作用完全不同,因此将它们进行比较并没有太多意义,尽管它们都与并发相关。这就好像说“为什么既有void又有public关键词”。 - DavidS
因此,volatile在某种程度上类似于类上的static关键字,可以让类的多个实例共享同一个变量/属性。 - Aruman
@Aruman,我认为那不正确。如果有多个线程读取一个static变量,那么它可能还必须是volatile,这两者是无关的。 - Nom1fan

192

挥发性(vɒlətʌɪl): 在常温下容易蒸发

volatile的重要点:

  1. 在Java中,可以使用Java关键字synchronizedvolatile以及锁来实现同步。
  2. 在Java中,我们不能拥有synchronized变量。使用synchronized关键字与变量是非法的,并将导致编译错误。在Java中,可以使用java volatile变量代替使用synchronized变量,这将指示JVM线程从主内存读取volatile变量的值,而不会将其缓存在本地。
  3. 如果一个变量不在多个线程之间共享,则无需使用volatile关键字。

source

volatile的示例用法:

public class Singleton {
    private static volatile Singleton _instance; // volatile variable
    public static Singleton getInstance() {
        if (_instance == null) {
            synchronized (Singleton.class) {
                if (_instance == null)
                    _instance = new Singleton();
            }
        }
        return _instance;
    }
}

我们在第一次请求时才会懒加载创建实例。
如果我们不将_instance变量设为volatile,那么创建Singleton实例的线程就无法与其他线程通信。因此,如果线程A正在创建Singleton实例,刚刚完成创建,而CPU出现了问题等,所有其他线程将无法看到_instance的值不是null,并且它们将认为它仍然被赋为null。
为什么会发生这种情况?因为读取线程没有进行任何锁定,在写入线程退出同步块之前,内存不会同步,_instance的值也不会在主内存中更新。使用Java中的Volatile关键字,Java自己处理这个问题,这样的更新将被所有读取线程看到。

结论: volatile关键字也用于在线程之间通信内存内容。

没有使用volatile的示例用法:

public class Singleton {    
    private static Singleton _instance;   //without volatile variable
    public static Singleton getInstance() {   
        if (_instance == null) {  
            synchronized(Singleton.class) {  
                if (_instance == null) 
                    _instance = new Singleton(); 
            } 
        }
        return _instance;  
    }
}

上面的代码不是线程安全的。虽然在 synchronized 块中再次检查了 instance 的值(出于性能考虑),但 JIT 编译器可能会重新排列字节码,使得对实例的引用在构造函数完成执行之前就被设置了。这意味着 getInstance() 方法返回的对象可能没有完全初始化。为了使代码线程安全,可以使用关键字 volatile,自 Java 5 以来用于实例变量。标记为 volatile 的变量仅在对象的构造函数完全执行后才对其他线程可见。
来源

enter image description here

Java 中的 volatile 用法:

快速失败的迭代器通常使用列表对象上的 volatile 计数器来实现。

  • 当列表被更新时,计数器将被递增。
  • 创建 Iterator 时,当前计数器的值将嵌入到 Iterator 对象中。
  • 进行 Iterator 操作时,该方法将比较两个计数器的值,并在它们不同时抛出 ConcurrentModificationException

故障安全迭代器的实现通常是轻量级的。它们通常依赖于特定列表实现数据结构的属性。没有一般模式。


2
"快速失败的迭代器通常使用易失性计数器实现" - 不再适用,成本太高:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6625725" - Vsevolod Golovanov
1
"这将指示JVM线程从主内存中读取volatile变量的值,并不在本地缓存它。" 好的观点。 - Humoyun Ahmad
2
为了实现线程安全,也可以使用private static final Singleton _instance; - Chris311
“刚创建后,CPU 就崩溃了” - 对不起,你在这里说什么?一个 CPU 核心突然“崩溃”了吗?这种情况发生过吗? - Stefan Reich
这个数字在某种程度上已经过时了。自Java 7以来,JVM中根本没有工作内存 - Jw C
显示剩余2条评论

67

volatile非常有用,可以停止线程。

虽然你不应该编写自己的线程,Java 1.6有很多好用的线程池。但是如果你确定需要一个线程,你将需要知道如何停止它。

我用于线程的模式是:

public class Foo extends Thread {

  private volatile boolean close = false;

  public void run() {
    while(!close) {
      // do work
    }
  }
  public void close() {
    close = true;
    // interrupt here if needed
  }
}
在上面的代码片段中,读取close的线程与调用close()的线程不同。没有使用volatile关键字,运行循环的线程可能永远看不到对close的更改。
注意,这里不需要同步。

2
我想知道为什么这是必要的。只有在其他线程必须以危及线程同步的方式对该线程的状态更改做出反应时,才需要这样做吗? - Jori
34
@Jori,你需要使用volatile,因为读取while循环内close的线程与调用close()的线程不同。如果没有使用volatile,运行while循环的线程可能永远无法看到对close的更改。 - Pyrolistical
1
你认为停止线程的方式和使用Thread#interrupt()和Thread#isInterrupted()方法之间有什么优劣之处吗? - Ricardo Belchior
4
你是否观察到了从未看到实践变化的线程?或者你能否扩展示例以可靠地触发该问题?我很好奇,因为我知道我曾经使用过(以及看到其他人使用过)与示例基本相同但没有“volatile”关键字的代码,它似乎总是正常工作。 - aroth
4
在当今的JVM中,即使是最简单的例子,你也能观察到这种行为,但是你不能可靠地再现它。对于更复杂的应用程序,你有时会在代码中有其他具有内存可见性保证的操作,这使得它可以正常工作,尤其危险的是,你不知道它为什么能够正常工作,代码中的一个简单而明显无关的改变就可能破坏你的应用程序... - Holger
显示剩余2条评论

47

volatile关键字声明的变量具有两个特性,使其变得特殊。

  1. 如果我们有一个volatile变量,它不能被任何线程缓存在计算机(微处理器)的高速缓存中。所有访问都来自主内存。

  2. 如果在一个volatile变量上有写操作,并且突然请求进行读操作,则保证写操作将在读操作之前完成

以上两点特性推断出来:

  • 所有读取volatile变量的线程一定会读取到最新的值。因为没有缓存值可以污染它。而且只有当前的写操作完成后,才会授予读取请求。

另一方面,

  • 如果我们进一步研究我提到的#2,我们可以看到,volatile关键字是维护一个共享变量的理想方式,该变量有'n'个读者线程和仅一个写者线程来访问。一旦添加了volatile关键字,就完成了。没有其他与线程安全相关的开销。

相反地,

我们不能仅使用volatile关键字来满足具有多个写者线程访问的共享变量


4
这解释了volatile和synchronized的区别。 - ajay
3
遗憾的是,这是不正确的。"volatile" 不控制缓存,也不为其他 CPU 的内存视图提供任何魔法即时全局更新。"volatile" 只是确保每次对变量的引用(读取或写入)时,JVM 将引用该变量在虚拟内存空间中分配的地址,而不是在寄存器或优化器选择的某个方便的影子位置(如堆栈)中存储的值,并且它也不会跳过引用的判断。 - sergey_o
3
如果没有“volatile”关键字,像“for (...) {a += b + c;}”这样的指令可能根本不涉及内存位置,只是在整个循环期间将“a”,“b”和“c”保留在寄存器中。当CPU向虚拟内存地址(或相应的物理内存地址)写入值时,更新并不会立即对其他CPU可见,也不会立即刷新到RAM [*]。 - sergey_o
2
更新被简单地放置到本地CPU的缓存中,然后排队到实现内存一致性协议(如MESI)的CPU互连中,协议消息开始传输到其他CPU,最终导致它们的缓存也被更新。这需要短暂但非零的时间。同时,其他CPU仍然不知道已经发生了更新。如果CPU1更新了易失性变量X,并且CPU2在稍后的时间读取它,CPU2可能会找到X的旧值或新值。 - sergey_o
2
要确保读取变量的“绝对最新”值的唯一方法是通过交互式操作,即通过锁定(“同步”,ReenterantLock等)或使用像AtomicInterger这样的交叉操作类。 - sergey_o
显示剩余2条评论

31

使用volatile的一个常见例子是将volatile boolean变量用作线程终止标志。如果您已经启动了一个线程,并且希望能够安全地从不同的线程中断它,可以让该线程定期检查一个标志。要停止线程,请将标志设置为true。通过将标志设置为volatile,您可以确保检查它的线程在下一次检查时看到它已经被设置,而无需甚至使用synchronized块。


19

Java Volatile

volatile -> synchronized[关于]

volatile 声明告诉程序员该值始终是最新的。问题在于该值可以保存在不同类型的硬件内存中。例如,它可以保存在 CPU 寄存器、CPU 缓存、RAM 中... CPU 寄存器和 CPU 缓存属于 CPU,无法共享数据,而 RAM 则在多线程环境下提供了帮助。

enter image description here

volatile 关键字表示一个变量将会被直接从/写入 RAM 内存。它有一些计算足迹。

Java 5 扩展了 volatile,支持 happens-before[关于]

对 volatile 字段的写操作 happens-before 每个后续对该字段的读取操作。

Read is after write

volatile 关键字不能解决 竞态条件[关于] 的问题,要解决这个问题需要使用 synchronized 关键字[关于]

因此,只有一个线程写入,其他线程只读取volatile值时才是安全的。


15

没有人提到长整型和双精度浮点型变量类型的读写操作处理。对于引用变量和大多数原始变量,读写是原子操作,但对于长整型和双精度浮点型变量类型,则必须使用volatile关键字才能实现原子操作。@link


为了让它更加清晰,没有必要设置布尔型的volatile,因为布尔型的读写已经是原子操作。 - Kai Wang
6
@KaiWang,你不需要在布尔变量上使用volatile以实现原子性。但出于可见性的考虑,你确实可能会这样做。这是你想要表达的意思吗? - SusanW

14

是的,当您希望多个线程访问可变变量时,必须使用 volatile。这并不是非常常见的用例,因为通常您需要执行不止一个原子操作(例如,在修改变量之前检查变量状态),在这种情况下,您将使用 synchronized 块。


11

在我的看法中,除了停止线程之外,使用volatile关键字的另外两个重要场景是:

  1. 双重检查锁定机制。常用于单例模式中。在这种情况下,单例对象需要声明为volatile
  2. 虚假唤醒。有时候线程会从wait调用中醒来,即使没有发出notify调用。这种行为称为虚假唤醒。可以通过使用条件变量(布尔标志)来解决这个问题。只要该标志仍为true,就将wait()调用放在while循环中。因此,如果线程由于除了Notify/NotifyAll之外的任何原因而从wait调用中醒来,则遇到标志仍为true,并且再次调用wait。在调用notify之前将该标志设置为true。在这种情况下,布尔标志被声明为volatile

整个第二部分似乎非常混乱,它混淆了丢失通知、虚假唤醒和内存可见性问题。此外,如果标志的所有用法都在同步块中,则volatile是多余的。我认为我理解了你的观点,但虚假唤醒不是正确的术语。请澄清一下。 - Nathan Hughes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接