volatile关键字对


Answers:


742

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

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

这本书“ Java并发实践,”我强烈建议,给人的一个很好的解释volatile。本书由撰写该问题中引用的IBM文章的同一人撰写(实际上,他在该书的底部引用了他的书)。我使用的volatile是他的文章所说的“模式1状态标志”。

如果您想了解有关幕后volatile工作原理的更多信息,请阅读Java内存模型。如果您想超越该级别,请阅读Hennessy&Patterson这样的优秀计算机体系结构书籍并阅读有关缓存一致性和缓存一致性的信息。


118
这个答案是正确的,但不完整。它忽略了volatileJSR 133中定义的新Java内存模型所带来的重要属性:当线程读取volatile变量时,它不仅看到其他线程最后写入该变量的值,而且还看到其他所有写入该变量的变量在volatile写入时在其他线程中可见。请参阅此答案此参考
亚当·扎克曼

46
对于初学者,我请您演示一些代码(请吗?)
Hungry Blue Dev

6
问题中链接的文章具有代码示例。
格雷格·马特斯

我认为“ Hennessy&Patterson”链接已断开。指向“ Java内存模型”的链接实际上导致了Oracle的Java语言规范“第17章,线程和锁”。
克里斯(Kris)

2
@fefrei:“立即”是一个俗语。当然,如果没有实际指定执行时间和线程调度算法,就无法保证。程序找出易失性读取是否在特定易失性写入之后的唯一方法是检查查看值是否为预期的已写入值。
Holger

177

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

如果您正在考虑使用volatile,请阅读有关java.util.concurrent原子行为的软件包。

Wikipedia上有关Singleton Pattern的帖子显示使用情况不稳定。


18
为什么同时有volatilesynchronized关键字?
ptkato 2015年

5
此后,关于Singleton模式的Wikipedia文章已经发生了很大变化,并且不再以上述volatile示例为特色。可以在存档版本中找到它。
bskp

1
@ptkato这两个关键字的用途完全不同,因此,尽管它们都与并发相关,但作为比较,这个问题没有多大意义。这就像说“为什么有两个voidpublic关键字”。
DavidS

134

有关的重点volatile

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

资源

示例用法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损坏等,所有其他线程将无法看到_instanceas 的值,而不是null,他们将认为该值仍被分配为null。

为什么会这样?因为读取器线程未进行任何锁定,并且直到写入器线程从同步块中出来,否则内存将不同步,并且_instance主内存中的值也不会更新。使用Java中的Volatile关键字,此关键字由Java本身处理,并且此类更新将在所有阅读器线程中可见。

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

上面的代码不是线程安全的。尽管出于性能原因,它会在同步块中再次检查实例的值(出于性能原因),但JIT编译器可以在构造函数完成执行之前以设置对实例的引用的方式重新排列字节码。这意味着方法getInstance()返回的对象可能尚未完全初始化。为了使代码具有线程安全性,从Java 5开始,可以将关键字volatile用于实例变量。一旦对象的构造函数完全完成其执行,标记为volatile的变量仅对其他线程可见。
资源

在此处输入图片说明

volatile在Java中的用法

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

  • 当列表更新时,计数器增加。
  • Iterator创建an时,计数器的当前值将嵌入到Iterator对象中。
  • Iterator执行操作,该方法比较两个计数器值和抛出一个ConcurrentModificationException如果它们是不同的。

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


2
“故障快速迭代器通常使用易失性计数器实现”-不再如此,太昂贵了:bugs.java.com/bugdatabase/view_bug.do?bug_id=6625725
Vsevolod Golovanov

_instance的双重检查是否安全?我认为即使挥发物也不安全
-Dexters

“这将指示JVM线程从主内存中读取易失性变量的值,而不是在本地对其进行缓存。” 好点
Humoyun Ahmad

为了线程安全,也可以使用private static final Singleton _instance;
Chris311 '18

53

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

在上面的代码段中,closewhile循环中的线程读取不同于调用的线程close()。如果没有volatile,则运行循环的线程可能永远不会看到更改关闭。

请注意,无需同步


2
我不知道为什么这是必要的。仅当其他线程必须以这种方式使线程同步处于危险中时对此线程的状态变化做出反应时,这不是唯一必要的吗?
2013年

27
@Jori,您需要volatile,因为while循环中的close线程读取与调用close()的线程不同。如果没有volatile,则运行循环的线程可能永远不会看到更改关闭。
火热2013年

您会说在停止像这样的线程或使用Thread#interrupt()和Thread#isInterrupted()方法之间有一个优势吗?
里卡多·贝尔基奥尔

2
@Pyrolistical-您是否观察到线程从未在实践中看到变化?还是可以扩展示例以可靠地触发该问题?我很好奇,因为我知道我使用(并看到其他人在使用)与示例基本相同但没有volatile关键字的代码,并且它似乎总是可以正常工作。
aroth '16

2
@aroth:使用当今的JVM,即使使用最简单的示例,您也可以在实践中观察到这一点,但是您无法可靠地重现此行为。对于更复杂的应用程序,有时您会在代码中执行其他操作,并保证内存具有可见性,从而保证它可以正常工作,这尤其危险,因为您不知道它为什么起作用,并且代码中简单,显然不相关的更改可能会破坏您的代码。应用程序…
Holger

31

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


27

volatile关键字声明的变量具有两个主要特性,使其与众不同。

  1. 如果我们有一个volatile变量,则任何线程都无法将其缓存到计算机的(微处理器)缓存中。访问总是从主存储器发生的。

  2. 如果对易失变量进行写操作,并且突然请求读操作,则可以保证写操作将在读操作之前完成

以上两个素质可以推论出

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

另一方面,

  • 如果我们进一步研究我提到的#2,我们可以看到volatile关键字是维护共享变量的理想方法,该共享变量具有n个读取器线程,只有一个写入器线程可以访问它。添加volatile关键字后,就完成了。没有关于线程安全性的任何其他开销。

相反,

我们不能volatile仅仅使用关键字来满足一个共享变量,该共享变量具有多个写入线程来访问它


3
这解释了易失性和同步之间的区别。
ajay

13

没有人提及长型和双变量类型的读写操作。读取和写入是引用变量和大多数原始变量的原子操作,长和双变量类型除外,它们必须使用volatile关键字进行原子操作。@链接


为了使它更加清晰,不需要设置布尔值volatile,因为对布尔值的读取和写入已经是原子的。
Kai Wang'4

2
@KaiWang您不需要出于原子性而在布尔值上使用volatile。但是您当然可能出于可见性原因。那是你想说的吗?
SusanW

12

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


10

我认为,除了停止使用volatile关键字的线程外,还有两个重要的方案:

  1. 双重检查锁定机制。通常用于Singleton设计模式。在这种情况下,需要单例对象声明为volatile
  2. 虚假的唤醒。即使没有发出通知调用,线程有时也可能从等待调用中唤醒。此行为称为伪唤醒。可以通过使用条件变量(布尔标志)来解决。只要标志为真,就将wait()调用放入while循环中。因此,如果线程由于Notify / NotifyAll以外的其他原因而从等待调用中唤醒,则它遇到的标志仍然为true,因此调用再次等待。在调用notify之前,将此标志设置为true。在这种情况下,布尔标志被声明为volatile

整个#2部分似乎非常混乱,它是在混合丢失的通知,虚假唤醒和内存可见性问题。同样,如果该标志的所有用法都处于同步状态,则volatile是多余的。我想我明白你的意思,但虚假唤醒并不是正确的说法。请说清楚。
内森·休斯

5

如果要开发多线程应用程序,则需要使用“ volatile”关键字或“ synchronized”关键字以及任何其他并发控制工具和技术。此类应用程序的示例是桌面应用程序。

如果您正在开发将部署到应用程序服务器(Tomcat,JBoss AS,Glassfish等)的应用程序,则不必自己处理并发控制,因为应用程序服务器已经解决了并发控制问题。实际上,如果我没记错的话,Java EE标准禁止在servlet和EJB中进行任何并发控制,因为它是“基础结构”层的一部分,您应该免于对其进行处理。如果要实现单例对象,则只能在此类应用程序中进行并发控制。如果您使用像Spring这样的框架来编织组件,这甚至已经解决了。

因此,在大多数Java开发中,其中应用程序是Web应用程序,并使用IoC框架(例如Spring或EJB),则不需要使用“ volatile”。


5

volatile仅保证所有线程,甚至它们自己,都在递增。例如:计数器同时看到变量的相同外观。它不用于代替同步,原子或其他东西,而是完全使读取同步。请不要将其与其他Java关键字进行比较。如下面的示例所示,易失变量操作也是原子的,它们立即失败或成功。

package io.netty.example.telnet;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static volatile  int a = 0;
    public static void main(String args[]) throws InterruptedException{

        List<Thread> list = new  ArrayList<Thread>();
        for(int i = 0 ; i<11 ;i++){
            list.add(new Pojo());
        }

        for (Thread thread : list) {
            thread.start();
        }

        Thread.sleep(20000);
        System.out.println(a);
    }
}
class Pojo extends Thread{
    int a = 10001;
    public void run() {
        while(a-->0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.a++;
            System.out.println("a = "+Main.a);
        }
    }
}

即使您放置不稳定,结果也总是会有所不同。但是,如果您按以下方式使用AtomicInteger,则结果将始终相同。这与同步也一样。

    package io.netty.example.telnet;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;

    public class Main {

        public static volatile  AtomicInteger a = new AtomicInteger(0);
        public static void main(String args[]) throws InterruptedException{

            List<Thread> list = new  ArrayList<Thread>();
            for(int i = 0 ; i<11 ;i++){
                list.add(new Pojo());
            }

            for (Thread thread : list) {
                thread.start();
            }

            Thread.sleep(20000);
            System.out.println(a.get());

        }
    }
    class Pojo extends Thread{
        int a = 10001;
        public void run() {
            while(a-->0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Main.a.incrementAndGet();
                System.out.println("a = "+Main.a);
            }
        }
    }

4

是的,我经常使用它-它对多线程代码非常有用。您所指的文章是一篇不错的文章。尽管有两点要牢记:

  1. 仅当您完全了解volatile以及它与同步有何不同时,才应使用volatile。在许多情况下,表面上看起来易失性是同步的更简单,更高性能的选择,而通常人们对易失性的更好理解会清楚地表明,同步是唯一可行的选择。
  2. volatile实际上在许多旧版JVM中均不起作用,尽管已同步。我记得看到过一个文档,其中引用了不同JVM中的各种支持级别,但不幸的是我现在找不到它。如果您使用的是Java 1.5之前的版本,或者您无法控制将要运行程序的JVM,则一定要仔细研究一下。

4

每个访问易失性字段的线程将在继续之前读取其当前值,而不是(潜在地)使用缓存的值。

只有成员变量可以是易失性或瞬态的。


3

绝对没错。(不仅在Java中,而且在C#中也是如此。)有时,您需要获取或设置一个值,该值必须保证是给定平台上的原子操作,例如int或boolean,但不需要线程锁定的开销。volatile关键字使您可以确保在读取值时获得的是当前值,而不是刚被另一个线程的写入使之过时的缓存值。


3

volatile关键字有两种不同的用法。

  1. 防止JVM从寄存器中读取值(假定为缓存),并强制从内存中读取其值。
  2. 降低内存不一致错误的风险。

防止JVM读取寄存器中的值,并强制从内存中读取其值。

一个忙标志用于防止线程持续而设备忙,且该标志不会被锁的保护:

while (busy) {
    /* do something else */
}

当另一个线程关闭busy标志时,测试线程将继续:

busy = 0;

但是,由于在测试线程中经常访问busy,因此JVM可以通过将busy的值放在寄存器中来优化测试,然后测试寄存器的内容,而无需在每次测试前读取内存中的busy值。测试线程永远不会看到繁忙更改,而另一个线程只会更改内存中的繁忙值,从而导致死锁。将忙碌标志声明为volatile会强制在每次测试之前读取其值。

降低内存一致性错误的风险。

使用易失性变量可降低内存一致性错误的风险,因为对易失性变量的任何写入都会与该变量的后续读取建立 “先发生”关系。这意味着对volatile变量的更改始终对其他线程可见。

没有内存一致性错误的读写技术称为原子动作

原子动作是一次有效地同时发生的动作。原子动作不能停在中间:它要么完全发生,要么根本不发生。直到动作完成,原子动作的副作用才可见。

您可以指定以下原子操作:

  • 对于参考变量和大多数原始变量(除long和double以外的所有类型),读写都是原子的。
  • 对于声明为volatile的所有变量 (包括long和double变量),读写都是原子的。

干杯!


3

volatile对程序员说,价值永远是最新的。问题在于该值可以保存在不同类型的硬件内存中。例如,它可以是CPU寄存器,CPU高速缓存,RAM ...СPU寄存器和CPU高速缓存属于CPU,并且不能共享数据,这与在多线程环境中可以抢救的RAM不同

在此处输入图片说明

volatile关键字表示直接在RAM内存中读写变量。它有一些计算足迹

Java 5volatile通过支持[关于]扩展happens-before

在每次对该字段的后续读取之前,都会对易失字段进行写操作。

volatile当多个线程可以同时写入一些值时,关键字不能解决这种race condition情况。答案是关键字[关于]synchronized

因此,仅当一个线程写入而其他线程仅读取该volatile值时,它才安全

易失性与同步


2

易失性确实在跟随。

1>不同线程对易失性变量的读写始终来自内存,而不是线程自己的缓存或cpu寄存器。因此,每个线程始终处理最新值。2>当2个不同的线程在堆中使用相同的实例或静态变量时,一个线程可能会看到其他线程的操作混乱。参见jeremy manson的博客。但是,volatile在这里有帮助。

下面的代码完全运行,显示了许多线程如何可以按预定义的顺序执行并打印输出,而无需使用synced关键字。

thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3

为此,我们可以使用以下完整的运行代码。

public class Solution {
    static volatile int counter = 0;
    static int print = 0;
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Thread[] ths = new Thread[4];
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new Thread(new MyRunnable(i, ths.length));
            ths[i].start();
        }
    }
    static class MyRunnable implements Runnable {
        final int thID;
        final int total;
        public MyRunnable(int id, int total) {
            thID = id;
            this.total = total;
        }
        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                if (thID == counter) {
                    System.out.println("thread " + thID + " prints " + print);
                    print++;
                    if (print == total)
                        print = 0;
                    counter++;
                    if (counter == total)
                        counter = 0;
                } else {
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        // log it
                    }
                }
            }
        }
    }
}

以下github链接具有自述文件,其中提供了适当的解释。 https://github.com/sankar4git/volatile_thread_ordering


1

从oracle文档页面,需要volatile变量来解决内存一致性问题:

使用易失性变量可以降低内存一致性错误的风险,因为对易失性变量的任何写入都会与该变量的后续读取建立先发生后关系。

这意味着对volatile变量的更改始终对其他线程可见。这也意味着,当线程读取一个volatile变量时,它不仅会看到对的最新更改volatile,而且还会看到导致更改的代码的副作用。

Peter Parker答案所解释,在没有volatile修饰符的情况下,每个线程的堆栈可能都有自己的变量副本。通过将变量设置为volatile,可以解决内存一致性问题。

参阅jenkov教程页面,以更好地理解。

请查看相关的SE问题,以获取有关volatile和使用volatile的用例的更多详细信息:

Java中的volatile和Synchronized之间的区别

一个实际的用例:

您有很多线程,它们需要以特定格式打印当前时间,例如:java.text.SimpleDateFormat("HH-mm-ss")。Yon可以具有一个类,该类将当前时间转换SimpleDateFormat为每秒钟并更新一次变量。所有其他线程可以简单地使用此volatile变量在日志文件中打印当前时间。


1

易变变量是轻量级同步。如果要求所有线程之间都具有最新数据可见性,并且原子性可能会受到影响,则在这种情况下,必须首选易变变量。对易失性变量的读取始终返回由任何线程完成的最新写入,因为它们既不缓存在寄存器中,也不缓存在其他处理器看不到的缓存中。挥发物是无锁的。当方案满足上述条件时,我将使用volatile。


-1

volatile键与变量一起使用时,将确保读取此变量的线程将看到相同的值。现在,如果您有多个线程读取和写入变量,则使变量volatile不够,数据将被破坏。映像线程读取了相同的值,但是每个线程都做了一些修改(例如增加了一个计数器),当写回内存时,数据完整性受到侵犯。这就是为什么有必要使变量同步(可能有不同的方式)的原因

如果更改是通过1个线程完成的,而其他线程只需要读取此值,则volatile将是合适的。



-2

下面是一个非常简单的代码,用于说明volatile对变量的要求,该变量用于控制其他线程的线程执行(这volatile是需要的一种情况)。

// Code to prove importance of 'volatile' when state of one thread is being mutated from another thread.
// Try running this class with and without 'volatile' for 'state' property of Task class.
public class VolatileTest {
    public static void main(String[] a) throws Exception {
        Task task = new Task();
        new Thread(task).start();

        Thread.sleep(500);
        long stoppedOn = System.nanoTime();

        task.stop(); // -----> do this to stop the thread

        System.out.println("Stopping on: " + stoppedOn);
    }
}

class Task implements Runnable {
    // Try running with and without 'volatile' here
    private volatile boolean state = true;
    private int i = 0;

    public void stop() {
        state = false;
    } 

    @Override
    public void run() {
        while(state) {
            i++;
        }
        System.out.println(i + "> Stopped on: " + System.nanoTime());
    }
}

volatile不使用:你永远不会看到“ 停在:XXX ”即使在“消息停止上:XXX ”,并且程序继续运行。

Stopping on: 1895303906650500

volatile使用:你会看到“ 停在:XXX ”马上。

Stopping on: 1895285647980000
324565439> Stopped on: 1895285648087300

演示:https//repl.it/repls/SilverAgonizingObjectcode


致低落选民:解释为什么会落选?如果这不是真的,那么至少我会学到什么地方出了问题。我已经两次添加了相同的评论,但不知道是谁一次又一次地删除
manikanta

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.