什么是volatile关键字有用

时间:2008-09-20 00:41:02

标签: java multithreading keyword volatile

今天上班时,我遇到了Java中的volatile关键字。我不太熟悉它,我找到了这个解释:

  

Java theory and practice: Managing volatility

鉴于该文章解释了相关关键字的详细信息,您是否使用过它,或者您是否曾经看到过以正确方式使用此关键字的情况?

26 个答案:

答案 0 :(得分:691)

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

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

我强烈推荐的这本书“Java Concurrency in Practice”对volatile给出了很好的解释。本书由撰写问题中引用的IBM文章的同一人撰写(事实上,他引用了该文章底部的书)。我对volatile的使用就是他的文章所称的“模式1状态标志”。

如果您想详细了解volatile的工作原理,请阅读the Java memory model。如果你想超越这个级别,请查看一本好的计算机体系结构书籍,如Hennessy & Patterson,并阅读缓存一致性和缓存一致性。

答案 1 :(得分:162)

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

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

Singleton Pattern上的维基百科帖子显示在使用中不稳定。

答案 2 :(得分:104)

关于volatile的重点:

  1. 使用Java关键字synchronizedvolatile并锁定,可以实现Java中的同步。
  2. 在Java中,我们不能拥有synchronized变量。对变量使用synchronized关键字是非法的,将导致编译错误。您可以使用java synchronized变量代替在Java中使用volatile变量,该变量将指示JVM线程从主内存中读取volatile变量的值,而不是缓存它本地。
  3. 如果多个线程之间没有共享变量,则无需使用volatile关键字。
  4. 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实例的Thread无法与其他线程通信。因此,如果线程A正在创建Singleton实例,并且在创建之后,CPU会损坏等,则所有其他线程将无法将_instance的值视为非空,并且他们将认为它仍然被指定为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块中再次检查实例的值(出于性能原因),但JIT编译器可以重新排列字节码,方式是在构造函数完成执行之前设置对实例的引用。这意味着方法getInstance()返回一个可能尚未完全初始化的对象。为了使代码具有线程安全性,可以使用关键字volatile,因为Java 5用于实例变量。标记为volatile的变量只有在对象的构造函数完全执行完毕后才能对其他线程可见 Source

    enter image description here

    volatile在Java中的使用

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

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

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

答案 3 :(得分:49)

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

注意不需要同步

答案 4 :(得分:30)

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

答案 5 :(得分:13)

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

  1. 如果我们有一个volatile变量,它就不能被任何线程缓存到计算机的(微处理器)缓存中。访问总是发生在主存储器中。

  2. 如果 写入操作 进入volatile变量,突然 读取操作 ,保证 写入操作将在读取操作之前完成

  3. 以上两个品质推断

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

    而另一方面,

    • 如果我们进一步调查我提到的#2 ,我们可以看到volatile关键字是维护共享变量的理想方式,该变量具有 & #39; N'读取线程数和只有一个写入线程 来访问它。添加volatile关键字后,即可完成。关于线程安全没有任何其他开销。

    相反,

    我们 无法 单独使用volatile关键字,以满足 不止一个的共享变量编写访问它的线程

答案 6 :(得分:12)

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

答案 7 :(得分:12)

没有人提到长和双变量类型的读写操作的处理。读取和写入是参考变量和大多数原始变量的原子操作,但长变量和双变量类型除外,它们必须使用volatile关键字作为原子操作。 @link

答案 8 :(得分:9)

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

  1. Double-checked locking mechanism。经常用于Singleton设计 图案。在此,单例对象需要声明为volatile
  2. Spurious Wakeups。即使没有发出通知呼叫,线程有时也会从等待呼叫中唤醒。这种行为称为supurious wakeup。这可以通过使用条件变量(布尔标志)来抵消。只要标志为真,就将wait()调用放入while循环中。因此,如果由于除了notify / notifyall之外的任何原因线程从等待调用中唤醒,那么它遇到标志仍然是真的,因此再次调用wait。在调用notify之前,将此标志设置为true。在这种情况下,布尔标志被声明为volatile

答案 9 :(得分:5)

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

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

因此,在Java开发的大多数情况下,应用程序是Web应用程序并使用Spring或EJB等IoC框架,您不需要使用'volatile'。

答案 10 :(得分: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);
            }
        }
    }

答案 11 :(得分:4)

是的,我使用它非常多 - 它对多线程代码非常有用。你指出的文章很好。虽然有两件重要的事情要记住:

  1. 你应该只使用volatile 完全明白它的作用 以及它与同步的不同之处。 在许多情况下出现挥发性, 从表面上看,更简单一点 高效的替代品 同步,往往更好 理解挥发性会使 明确同步是唯一的 可行的选项。
  2. volatile实际上并不适用于 但是,很多较旧的JVM 同步了。我记得看到一个文档引用了不同JVM中的各种级别的支持,但遗憾的是我现在找不到它。如果您正在使用Java pre 1.5,或者如果您无法控制运行程序的JVM,请务必查看它。

答案 12 :(得分:3)

当然,是的。 (而且不只是在Java中,而且在C#中。)有时你需要获取或设置一个值,保证在你的给定平台上是一个原子操作,例如int或boolean,但不要求线程锁定的开销。 volatile关键字允许您确保在读取值时获得当前值,而不是通过在另一个线程上写入而过时的缓存值。

答案 13 :(得分:3)

访问volatile字段的每个线程都将在继续之前读取其当前值,而不是(可能)使用缓存值。

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

答案 14 :(得分:2)

易失性变量是轻量级同步。当需要在所有线程中查看最新数据并且原子性可能受到损害时,在这种情况下,必须首选Volatile Variables。读取volatile变量总是返回由任何线程完成的最近写入,因为它们既不缓存在寄存器中也不缓存在其他处理器看不到的缓存中。易失性是无锁的。当场景符合上面提到的标准时,我使用volatile。

答案 15 :(得分:2)

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

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

    阻止JVM读取寄存器中的值,并强制它   要从内存中读取的值。

    忙标志用于防止线程在设备繁忙时继续,并且标志不受锁保护:

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

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

    busy = 0;
    

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

      

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

    使用volatile变量可以降低内存一致性错误的风险,因为对volatile变量的任何写入都会建立一个 &#34;发生之前&#34; 与后续读取同一变量的关系。这意味着对volatile变量的更改始终对其他线程可见。

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

    原子动作是一次有效发生的动作。原子动作不能在中间停止:它要么完全发生,要么根本不发生。在动作完成之前,原子动作的副作用是不可见的。

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

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

    干杯!

答案 16 :(得分:2)

挥发性确实跟随。

1&GT;不同线程对volatile变量的读写总是来自内存,而不是来自线程自己的缓存或cpu寄存器。所以每个线程总是处理最新的值。 2 - ;当2个不同的线程在堆中使用相同的实例或静态变量时,可能会将其他操作视为乱序。请参阅jeremy manson的博客。但是挥发性有帮助。

完全运行代码后,显示了多个线程如何以预定义顺序执行并打印输出而不使用synchronized关键字。

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

答案 17 :(得分:1)

从oracle文档page开始,需要使用volatile变量来修复内存一致性问题:

  

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

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

Peter Parker回答中所述,在没有volatile修饰符的情况下,每个线程的堆栈可能都有自己的变量副本。通过将变量设为volatile,内存一致性问题已得到修复。

查看jenkov教程页面以便更好地理解。

查看相关的SE问题,了解有关volatile&amp; amp ;;的详细信息。用例使用volatile:

Difference between volatile and synchronized in Java

一个实际用例:

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

答案 18 :(得分:1)

虽然我在这里提到的答案中看到许多很好的理论解释,但我在此处添加了一个带有示例的实际示例:

1。

无需大量使用代码即可运行

public class VisibilityDemonstration {

private static int sCount = 0;

public static void main(String[] args) {
    new Consumer().start();
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        return;
    }
    new Producer().start();
}

static class Consumer extends Thread {
    @Override
    public void run() {
        int localValue = -1;
        while (true) {
            if (localValue != sCount) {
                System.out.println("Consumer: detected count change " + sCount);
                localValue = sCount;
            }
            if (sCount >= 5) {
                break;
            }
        }
        System.out.println("Consumer: terminating");
    }
}

static class Producer extends Thread {
    @Override
    public void run() {
        while (sCount < 5) {
            int localValue = sCount;
            localValue++;
            System.out.println("Producer: incrementing count to " + localValue);
            sCount = localValue;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                return;
            }
        }
        System.out.println("Producer: terminating");
    }
}
}

在上面的代码中,有两个线程-Producer和Consumer。

生产者线程在两次循环之间循环5次(睡眠时间为1000毫秒或1秒)。在每次迭代中,生产者线程都会将sCount变量的值增加1。因此,在所有迭代中,生产者线程都会将sCount的值从0更改为5

使用者线程处于恒定循环中,只要sCount的值更改,就打印直到该值达到5为止。

两个循环都同时开始。因此,生产者和消费者都应将sCount的值打印5次。

输出

Consumer: detected count change 0
Producer: incrementing count to 1
Producer: incrementing count to 2
Producer: incrementing count to 3
Producer: incrementing count to 4
Producer: incrementing count to 5
Producer: terminating

分析

在上面的程序中,当生产者线程更新sCount的值时,它确实更新了主内存中的变量的值(每个线程将从其初始读取变量值的内存)。但是,使用者线程仅在第一次从该主内存中读取sCount的值,然后将该变量的值缓存在其自己的内存中。因此,即使生产者线程已经更新了主存储器中原始sCount的值,消费者线程也将从其未更新的缓存值中读取。这称为可见性问题

2。

代码运行具有可变性

在上面的代码中,用以下代码替换声明sCount的代码行:

private volatile  static int sCount = 0;

输出

Consumer: detected count change 0
Producer: incrementing count to 1
Consumer: detected count change 1
Producer: incrementing count to 2
Consumer: detected count change 2
Producer: incrementing count to 3
Consumer: detected count change 3
Producer: incrementing count to 4
Consumer: detected count change 4
Producer: incrementing count to 5
Consumer: detected count change 5
Consumer: terminating
Producer: terminating

分析

当我们将变量声明为volatile时,这意味着对该变量或对该变量的所有读取和所有写入都将直接进入主存储器。这些变量的值将永远不会被缓存。

由于sCount变量的值永远不会被任何线程缓存,因此使用者始终会从主内存(生产者线程在其中更新该值)中读取sCount的原始值。因此,在这种情况下,两个线程都将sCount的不同值打印5次,输出是正确的。

通过这种方式,volatile关键字解决了可见性问题

答案 19 :(得分:0)

通过在Java应用程序中并发运行线程来异步修改易失性变量。不允许变量的本地副本与&#34; main&#34;中当前保存的值不同。记忆。实际上,声明为volatile的变量必须使其数据在所有线程之间同步,这样无论何时在任何线程中访问或更新变量,所有其他线程都会立即看到相同的值。当然,很可能volatile变量的访问和更新开销比&#34; plain&#34;变量,因为线程可以拥有自己的数据副本的原因是为了提高效率。

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

供参考,请参阅此http://techno-terminal.blogspot.in/2015/11/what-are-volatile-variables.html

答案 20 :(得分:0)

当与变量一起使用时,volatile键将确保读取此变量的线程将看到相同的值。现在,如果您有多个线程读取和写入变量,使变量volatile变得不够,数据将被破坏。图像线程已经读取了相同的值,但每个都已经完成了一些操作(比如增加了一个计数器),当写回内存时,违反了数据完整性。这就是为什么有必要使变量同步(不同的方式是可能的)

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

答案 21 :(得分:0)

我喜欢Jenkov's explanation

  

Java volatile 关键字用于将Java变量标记为&#34;存储在主存储器&#34;中。更准确地说,这意味着,每次读取一个易失性变量都将从计算机的主存储器读取,而不是从CPU缓存读取,并且每次写入一个易失性变量都将被写入主存储器,而不仅仅是到CPU缓存。

     

实际上,自Java 5起,volatile关键字不仅仅保证了这一点   volatile变量被写入主存储器并从主存储器读取。

这是扩展的可见性保证所谓的事先保证。

  

易失性

的性能考虑因素      

读取和写入volatile变量会导致变量被读取或写入主存储器。读取和写入主内存比访问CPU缓存更昂贵。访问volatile变量也会阻止指令重新排序,这是一种正常的性能增强技术。因此,在真正需要强制实施变量可见性时,应该只使用volatile变量。

答案 22 :(得分:0)

volatile变量基本上在更新后基本上用于主共享缓存行中的即时更新(刷新),因此更改会立即反映到所有工作线程中。

答案 23 :(得分:0)

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

enter image description here

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

Java 5通过支持volatile [About]扩展了happens-before

  

对易失性字段的写操作发生在每次对该字段的后续读取之前。

当多个线程可以同时写入某些值时,

volatile关键字无法解决race condition的情况。答案是synchronized关键字[About]

因此,仅当一个线程写入而其他线程仅读取volatile

时,它才安全。

volatile vs synchronized

答案 24 :(得分:0)

如果您没有为共享变量使用volatile修饰符,则假定线程修改了共享变量的值。当其他线程要读取此变量的值时,它们看不到更新后的值,因为它们是从CPU的缓存而不是RAM内存中读取变量的值。此问题也称为Visibility Problem

通过声明共享变量volatile,所有对计数器变量的写操作将立即写回到主存储器。另外,计数器变量的所有读取都将直接从主存储器中读取。

public class SharedObject {
    public volatile int sharedVariable = 0;
}

对于非易失性变量,不能保证Java虚拟机(JVM)何时将数据从主内存读取到CPU缓存中,或何时将数据从CPU缓存写入到主内存中。这可能会导致一些问题,我将在以下各节中进行解释。


示例:

想象一下这样一种情况,其中两个或多个线程可以访问包含如下声明的计数器变量的共享对象:

public class SharedObject {
    public int counter = 0;
}

也可以想象,只有线程1会增加计数器变量,但是线程1和线程2都可能会不时读取计数器变量。

如果未将计数器变量声明为volatile,则无法保证何时将计数器变量的值从CPU高速缓存写回主存储器。这意味着,CPU缓存中的计数器变量值可能与主存储器中的不同。此处显示了这种情况:

volatile

由于尚未由另一个线程将变量的最新值写回主存储器而导致线程看不到变量的最新值的问题称为“可见性”问题。一个线程的更新对其他线程不可见。

答案 25 :(得分:-1)

下面是一个非常简单的代码,用于说明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时:即使在<< em>停止时间:xxx <之后,您也永远不会看到'停止时间:xxx ”消息。 / em>”,该程序将继续运行。

Stopping on: 1895303906650500

使用volatile时:,您会立即看到“ 停在:xxx ”。

Stopping on: 1895285647980000
324565439> Stopped on: 1895285648087300