Java中最简单易懂的volatile关键字示例

Posted

技术标签:

【中文标题】Java中最简单易懂的volatile关键字示例【英文标题】:Simplest and understandable example of volatile keyword in Java 【发布时间】:2013-07-18 20:33:26 【问题描述】:

我正在阅读 Java 中的 volatile 关键字并完全理解它的理论部分。

但是,我正在寻找的是一个很好的例子,它显示了如果变量不是 volatile 会发生什么,如果它是。

下面的代码 sn-p 不能按预期工作(取自here):

class Test extends Thread 

    boolean keepRunning = true;

    public void run() 
        while (keepRunning) 
        

        System.out.println("Thread terminated.");
    

    public static void main(String[] args) throws InterruptedException 
        Test t = new Test();
        t.start();
        Thread.sleep(1000);
        t.keepRunning = false;
        System.out.println("keepRunning set to false.");
    

理想情况下,如果keepRunning 不是volatile,线程应该无限期地继续运行。但是,它确实会在几秒钟后停止。

我有两个基本问题:

谁能用例子解释一下volatile?不是 JLS 的理论。 volatile 是否可以替代同步?是否实现了原子性?

【问题讨论】:

过去的帖子广泛讨论了它***.com/questions/7212155/java-threading-volatile 你在往回想。 理想情况下,如果 keepRunning 不是 volatile,线程应该无限期地继续运行。实际上,恰恰相反:添加volatile 保证 对字段的更改将是可见的。没有关键字,根本就没有任何保证,任何事情都可能发生;你不能说 thread 应该继续运行 [...]. 事情是这样的:内存可见性错误本质上很难(不可能?)通过一个每次都会失败的简单示例来演示。假设您有一台多核机器,如果您经常运行它(例如,运行 1000 次),您的示例可能至少会失败几次。如果你有一个大程序——例如,整个程序及其对象不适合 CPU 缓存——那么就会增加看到错误的可能性。基本上,并发错误是这样的,如果理论上说它可以中断,它可能会,但每隔几个月只有一次,并且可能在生产中。 已经列出了很好的例子***.com/questions/5816790/… 这是一个例子,上面写着vanillajava.blogspot.co.uk/2012/01/… 【参考方案1】:

Volatile --> 保证可见性而不是原子性

同步(锁定)-->保证可见性和原子性(如果做得好)

易失性不能替代同步

只有在更新引用并且不对它执行一些其他操作时才使用 volatile。

例子:

volatile int i = 0;

public void incrementI()
   i++;

如果不使用同步或 AtomicInteger 将不是线程安全的,因为递增是一种复合操作。

为什么程序不会无限期地运行?

这取决于各种情况。在大多数情况下,JVM 足够聪明,可以刷新内容。

Correct use of volatile 讨论了 volatile 的各种可能用途。正确使用 volatile 很棘手,我会说“如果有疑问,请不要使用它”,请改用同步块。

还有:

可以使用同步块代替 volatile,但反之则不然

【讨论】:

这是错误的。 volatile 保证原子性质。 Oracle 文档明确规定了这一点。见docs.oracle.com/javase/tutorial/essential/concurrency/…。 在 Java 中,当我们有多个线程时,每个线程都有自己的堆栈(内存空间),并且 init 每个线程都有自己可以访问的变量副本。如果没有 volatile 关键字来装饰 int i ,则每个线程都可以在执行中使用它。当使用 volatile 声明时,每个线程必须直接从主内存读取/写入 i 的值,而不是从本地副本读取/写入。所以从每个线程的角度来看,对变量 i 的操作都是原子的。 atomicity 部分答案令人困惑。同步为您提供互斥访问可见性volatile 仅提供可见性volatile 也使 longdouble 原子的读/写(同步也因其互斥性质而实现)。【参考方案2】:

对于您的特定示例:如果未声明为 volatile,则服务器 JVM 可以将 keepRunning 变量提升出循环,因为它未在循环中修改 in(将其变为无限循环),但客户端 JVM 不会。这就是为什么您会看到不同的结果。

关于 volatile 变量的一般解释如下:

当一个字段被声明为volatile 时,编译器和运行时会注意到这个变量是共享的,并且对它的操作不应该与其他内存操作重新排序。易失性变量不会缓存在寄存器或缓存中,它们对其他处理器是隐藏的,因此读取易失性变量总是返回任何线程最近的写入

volatile 变量的可见性影响超出了 volatile 变量本身的值。当线程 A 写入 volatile 变量,随后线程 B 读取相同的变量时,在写入 volatile 变量之前对 A 可见的所有变量的值在读取 volatile 变量后对 B 可见。

volatile 变量最常见的用途是作为完成、中断或状态标志:

  volatile boolean flag;
  while (!flag)  
     // do something untill flag is true
  

易失性变量可用于其他类型的状态信息,但在尝试此操作时需要更加小心。例如,volatile 的语义不足以使增量操作 (count++) 成为原子操作,除非您可以保证该变量仅从单个线程写入。

加锁可以同时保证可见性和原子性; volatile 变量只能保证可见性。

只有在满足以下所有条件时才能使用 volatile 变量:

对变量的写入不依赖于其当前值,或者您可以 确保只有一个线程更新该值; 该变量与其他状态变量不参与不变量;和 在访问变量时,出于任何其他原因不需要锁定。

调试提示:确保在调用 JVM 时始终指定 -server JVM 命令行开关,即使是用于开发和测试。服务端 JVM 比客户端 JVM 执行了更多的优化,比如将没有在循环中修改的变量提升到循环之外;可能在开发环境(客户端 JVM)中工作的代码可能会在部署环境中中断 (服务器 JVM)。

这是"Java Concurrency in Practice" 的摘录,这是你能找到的关于这个主题的最好的书。

【讨论】:

【参考方案3】:

我稍微修改了你的例子。现在将keepRunning 用作易失性和非易失性成员的示例:

class TestVolatile extends Thread
    //volatile
    boolean keepRunning = true;

    public void run() 
        long count=0;
        while (keepRunning) 
            count++;
        

        System.out.println("Thread terminated." + count);
    

    public static void main(String[] args) throws InterruptedException 
        TestVolatile t = new TestVolatile();
        t.start();
        Thread.sleep(1000);
        System.out.println("after sleeping in main");
        t.keepRunning = false;
        t.join();
        System.out.println("keepRunning set to " + t.keepRunning);
    

【讨论】:

很好的例子。这对我很有效。没有 volatile on keepRunning 线程将永远挂起。一旦您将 keepRunning 标记为 volatile - 它会在 t.keepRunning = false; 之后停止 示例为我工作,一直在寻找工作示例。 +1 因为它帮助了我,缺乏解释并没有伤害,也不值得投反对票。 嗨 paritosht 和@John Doe,您能帮忙解释一下为什么您的代码是一个工作示例吗?当我的机器执行问题中提供的代码时,不管有没有 volatile 关键字,它都会停止。 我在此处使用votalite 得到相同的结果【参考方案4】:

volatile 关键字是什么? volatile 关键字可防止缓存变量。

考虑这段代码,首先没有volatile关键字:

class MyThread extends Thread 
    private boolean running = true;   //non-volatile keyword

    public void run() 
        while (running) 
            System.out.println("hello");
        
    

    public void shutdown() 
        running = false;
    


public class Main 

    public static void main(String[] args) 
        MyThread obj = new MyThread();
        obj.start();

        Scanner input = new Scanner(System.in);
        input.nextLine(); 
        obj.shutdown();   
        

理想情况下,这个程序应该打印hello,直到按下Return键。但在某些机器上,变量running 可能会被缓存,您无法通过shutdown() 方法更改其值,这会导致hello 文本的无限打印。

因此,通过使用volatile 关键字,可以保证您的变量不会被缓存,并且代码在所有机器上都可以正常运行。

private volatile boolean running = true;  //volatile keyword

使用volatile 关键字是一种良好且安全的编程习惯。

【讨论】:

“使用volatile 关键字是一种良好且安全的编程习惯。”听起来您应该明确地将其添加到所有变量中。如果您有其他方法来确保数据同步并且出于性能原因想要缓存,volatile 并不好或不安全。 volatile 是一个可以像其他任何东西一样被滥用的工具。【参考方案5】:

Variable Volatile:Volatile 关键字适用于变量。 Java 中的 volatile 关键字保证 volatile 变量的值总是从主内存中读取,而不是从 Thread 的本地缓存中读取。

Access_Modifier volatile DataType Variable_Name;

易失性字段:向 VM 指示多个线程可能会尝试同时访问/更新该字段的值。一种特殊的实例变量,它必须在所有线程之间共享,具有修改的值。类似于静态(类)变量,只有一个易失性值的副本缓存在主存储器中,因此在执行任何 ALU 操作之前,每个线程必须在 ALU 操作之后从主存储器读取更新的值,它必须直接写入主存储器。 (对 volatile 变量 v 的写入与任何线程对 v 的所有后续读取同步)这意味着对 volatile 变量的更改始终对其他线程可见。

这里是nonvoltaile variable 如果线程 t1 更改了 t1 缓存中的值,线程 t2 无法访问更改的值,直到 t1 写入,t2 从主内存读取最近修改的值,这可能导致@987654335 @。

volatile cannot be cached - assembler

    +--------------+--------+-------------------------------------+
    |  Flag Name   |  Value | Interpretation                      |
    +--------------+--------+-------------------------------------+
    | ACC_VOLATILE | 0x0040 | Declared volatile; cannot be cached.|
    +--------------+--------+-------------------------------------+
    |ACC_TRANSIENT | 0x0080 | Declared transient; not written or  |
    |              |        | read by a persistent object manager.|
    +--------------+--------+-------------------------------------+

Shared Variables: 可以在线程之间共享的内存称为共享内存或堆内存。所有实例字段、静态字段和数组元素都存储在堆内存中。

Synchronization: synchronized 适用于方法、块。允许在对象上一次只执行 1 个线程。如果 t1 获得控制权,那么剩余线程必须等待直到它释放控制权。

例子:

public class VolatileTest implements Runnable 

    private static final int MegaBytes = 10241024;

    private static final Object counterLock = new Object();
    private static int counter = 0;
    private static volatile int counter1 = 0;

    private volatile int counter2 = 0;
    private int counter3 = 0;

    @Override
    public void run() 
        for (int i = 0; i < 5; i++) 
            concurrentMethodWrong();
        

    

    void addInstanceVolatile() 
        synchronized (counterLock) 
            counter2 = counter2 + 1;
            System.out.println( Thread.currentThread().getName() +"\t\t « InstanceVolatile :: "+ counter2);
        
    

    public void concurrentMethodWrong() 
        counter = counter + 1;
        System.out.println( Thread.currentThread().getName() +" « Static :: "+ counter);
        sleepThread( 1/4 );

        counter1 = counter1 + 1;
        System.out.println( Thread.currentThread().getName() +"\t « StaticVolatile :: "+ counter1);
        sleepThread( 1/4 );

        addInstanceVolatile();
        sleepThread( 1/4 );

        counter3 = counter3 + 1;
        sleepThread( 1/4 );
        System.out.println( Thread.currentThread().getName() +"\t\t\t\t\t « Instance :: "+ counter3);
    
    public static void main(String[] args) throws InterruptedException 
        Runtime runtime = Runtime.getRuntime();

        int availableProcessors = runtime.availableProcessors();
        System.out.println("availableProcessors :: "+availableProcessors);
        System.out.println("MAX JVM will attempt to use : "+ runtime.maxMemory() / MegaBytes );
        System.out.println("JVM totalMemory also equals to initial heap size of JVM : "+ runtime.totalMemory() / MegaBytes );
        System.out.println("Returns the amount of free memory in the JVM : "+ untime.freeMemory() / MegaBytes );
        System.out.println(" ===== ----- ===== ");

        VolatileTest volatileTest = new VolatileTest();
        Thread t1 = new Thread( volatileTest );
        t1.start();

        Thread t2 = new Thread( volatileTest );
        t2.start();

        Thread t3 = new Thread( volatileTest );
        t3.start();

        Thread t4 = new Thread( volatileTest );
        t4.start();

        Thread.sleep( 10 );;

        Thread optimizeation = new Thread() 
            @Override public void run() 
                System.out.println("Thread Start.");

                Integer appendingVal = volatileTest.counter2 + volatileTest.counter2 + volatileTest.counter2;

                System.out.println("End of Thread." + appendingVal);
            
        ;
        optimizeation.start();
    

    public void sleepThread( long sec ) 
        try 
            Thread.sleep( sec * 1000 );
         catch (InterruptedException e) 
            e.printStackTrace();
        
    

Static[Class Field] vs Volatile[Instance Field] - 两者都没有被线程缓存

静态字段对所有线程都是通用的,并存储在方法区中。 static 与 volatile 没用。静态字段不能序列化。

Volatile 主要与存储在堆区域中的实例变量一起使用。 volatile 的主要用途是维护所有线程的更新值。实例 volatile 字段可以是 Serialized。

@see

Volatile Vs Static in java System with multiple cores sharing an L2 cache JVM memory model

【讨论】:

【参考方案6】:

理想情况下,如果 keepRunning 不是 volatile,线程应该无限期地继续运行。但是,它确实会在几秒钟后停止。

如果您在单处理器中运行,或者您的系统非常繁忙,则操作系统可能会换出线程,这会导致某些级别的缓存失效。没有volatile 并不意味着内存将共享,但 JVM 会出于性能原因尝试不同步内存,因此内存可能不会被更新。

另外需要注意的是System.out.println(...) 是同步的,因为底层PrintStream 进行同步以停止重叠输出。因此,您可以在主线程中“免费”获得内存同步。然而,这仍然不能解释为什么阅读循环会看到更新。

无论println(...) 行是输入还是输出,您的程序都会在配备 Intel i7 的 MacBook Pro 上的 Java6 下为我运行。

任何人都可以用例子解释 volatile 吗?不是 JLS 的理论。

我认为你的例子很好。不知道为什么它不适用于删除所有 System.out.println(...) 语句。它对我有用。

volatile 是同步的替代品吗?是否实现了原子性?

在内存同步方面,volatile 抛出与synchronized 块相同的内存屏障,除了volatile 屏障是单向的而不是双向的。 volatile 读取会引发负载屏障,而写入会引发存储屏障。 synchronized 块是添加了互斥锁的双向屏障。

然而,就atomicity 而言,答案是“视情况而定”。如果您正在从字段中读取或写入值,则 volatile 提供适当的原子性。但是,增加volatile 字段会受到++ 实际上是3 个操作的限制:读取、递增、写入。在这种情况下或更复杂的互斥体情况下,可能需要一个完整的synchronized 块。 AtomicInteger 通过复杂的测试和设置自旋循环解决了 ++ 问题。

【讨论】:

我评论了两个 SOPln 语句,但它仍然在几秒钟后停止..你能告诉我一个可以按预期工作的例子吗? 您是否在单处理器系统上运行@tm99?因为你的程序在 Macbook Pro Java6 上为我永远旋转。 我在 Win Xp 32 位 Java 6 上运行 “任何同步块(或任何易失性字段)都会导致所有内存同步”——你确定吗?你会提供 JLS 参考吗?据我所知,唯一的保证是在释放锁 L1 之前对内存进行的修改在线程获得 same 锁 L1 之后对它们是可见的;使用 volatiles,在 volatile 写入 F1 之前的所有内存修改在对 same 字段 F1 进行 volatile 读取之后对线程可见,这与说 all* 非常不同内存已同步。它不像任何线程运行同步块那么简单。 any 内存屏障被越过(synchronizedvolatile)时,all 内存存在“发生在之前”的关系。除非您锁定在同一监视器上,否则无法保证锁和同步的 顺序,这就是您所指的@BrunoReis。但如果println(...) 完成,则可以保证keepRunning 字段已更新。【参考方案7】:

当变量为volatile时,保证不会被缓存,不同的线程会看到更新后的值。但是,不标记它volatile 并不能保证相反。 volatile 是那些在 JVM 中被破坏了很长时间但仍然没有被很好理解的东西之一。

【讨论】:

在现代多处理器@Jeff 中,您的最后一条评论有些错误/误导。 JVM 非常聪明地刷新值,因为这样做会影响性能。 当 main 将 keepRunning 设置为 false 时,线程仍会看到更新,因为 JVM 很聪明地刷新值。但这并不能保证(请参阅上面@Gray 的评论)。【参考方案8】:

volatile 不一定会产生巨大的变化,具体取决于 JVM 和编译器。但是,对于许多(边缘)情况,优化导致变量的更改未能被注意到与正确写入之间可能是不同的。

基本上,优化器可以选择将非易失性变量放在寄存器或堆栈上。如果另一个线程在堆或类的原语中更改它们,另一个线程将继续在堆栈上查找它,并且它会过时。

volatile 确保不会发生此类优化,并且所有读取和写入都直接到堆或所有线程都会看到的其他地方。

【讨论】:

【参考方案9】:

很多很好的例子,但我只想补充一点,有很多场景需要volatile,所以没有一个具体的例子来统治它们。

    您可以使用volatile 强制所有线程从主内存中获取变量的最新值。 您可以使用synchronization 来保护关键数据 您可以使用Lock API 你可以使用Atomic变量

查看更多Java volatile examples。

【讨论】:

【参考方案10】:

请在下面找到解决方案,

这个变量的值永远不会被缓存在线程本地:所有的读取和写入都将直接进入“主内存”。 volatile 强制线程每次更新原始变量。

public class VolatileDemo 

    private static volatile int MY_INT = 0;

    public static void main(String[] args) 

        ChangeMaker changeMaker = new ChangeMaker();
        changeMaker.start();

        ChangeListener changeListener = new ChangeListener();
        changeListener.start();

    

    static class ChangeMaker extends Thread 

        @Override
        public void run() 
            while (MY_INT < 5)
                System.out.println("Incrementing MY_INT "+ ++MY_INT);
                try
                    Thread.sleep(1000);
                catch(InterruptedException exception) 
                    exception.printStackTrace();
                
            
        
    

    static class ChangeListener extends Thread 

        int local_value = MY_INT;

        @Override
        public void run() 
            while ( MY_INT < 5)
                if( local_value!= MY_INT)
                    System.out.println("Got Change for MY_INT "+ MY_INT);
                    local_value = MY_INT;
                
            
        
    


请参考此链接http://java.dzone.com/articles/java-volatile-keyword-0 以获得更清晰的信息。

【讨论】:

虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。 是的,你完全正确。我会添加它。感谢您的宝贵意见。【参考方案11】:

volatile关键字告诉JVM它可能被另一个线程修改。 每个线程都有自己的堆栈,因此它可以访问自己的变量副本。创建线程时,它会将所有可访问变量的值复制到自己的内存中。

public class VolatileTest 
    private static final Logger LOGGER = MyLoggerFactory.getSimplestLogger();

    private static volatile int MY_INT = 0;

    public static void main(String[] args) 
        new ChangeListener().start();
        new ChangeMaker().start();
    

    static class ChangeListener extends Thread 
        @Override
        public void run() 
            int local_value = MY_INT;
            while ( local_value < 5)
                if( local_value!= MY_INT)
                    LOGGER.log(Level.INFO,"Got Change for MY_INT : 0", MY_INT);
                     local_value= MY_INT;
                
            
        
    

    static class ChangeMaker extends Thread
        @Override
        public void run() 

            int local_value = MY_INT;
            while (MY_INT <5)
                LOGGER.log(Level.INFO, "Incrementing MY_INT to 0", local_value+1);
                MY_INT = ++local_value;
                try 
                    Thread.sleep(500);
                 catch (InterruptedException e)  e.printStackTrace(); 
            
        
    

试试这个有和没有 volatile 的例子。

【讨论】:

【参考方案12】:
public class VolatileDemo 
    static class Processor 
        //without volatile program keeps running on my platform
        private boolean flag = false;

        public void setFlag() 
            System.out.println("setting flag true");
            this.flag = true;
        

        public void process() 
            while(!flag) 
                int x = 5;
                // using sleep or sout will end the program without volatile.
                // Probably these operations, cause thread to be rescheduled, read from memory. Thus read new flag value and end.
            

            System.out.println("Ending");
        
    

    public static void main(String[] args) throws InterruptedException 
        Processor processor = new Processor();
        Thread t1 = new Thread(processor::process);

        t1.start();

        Thread.sleep(2000);
        processor.setFlag();

    

【讨论】:

【参考方案13】:

声明为 volatile 的对象通常用于在线程之间传递状态信息,以确保 CPU 缓存被更新,即保持同步,在存在 volatile 字段的情况下,一条 CPU 指令,一个内存屏障,通常称为一个 membar 或栅栏,被发射以通过 volatile 字段值的更改来更新 CPU 缓存。

volatile 修饰符告诉编译器,被 volatile 修改的变量可能会被程序的其他部分意外更改。

volatile 变量只能在线程上下文中使用。看例子here

【讨论】:

缓存在现代 CPU 上始终保持同步,与 volatile 无关。

以上是关于Java中最简单易懂的volatile关键字示例的主要内容,如果未能解决你的问题,请参考以下文章

java中volatile关键字,你真的了解吗?volatile原理剖析实例讲解(简单易懂)

java中volatile关键字,你真的了解吗?volatile原理剖析实例讲解(简单易懂)

面试必问的 volatile 关键字,通俗易懂,看完还不懂你打我。

volatile总结

Java并发编程:volatile关键字解析

Java并发编程:volatile关键字解析