【问题标题】:Why doesn't volatile in Java update the value of a variable?为什么 Java 中的 volatile 不更新变量的值?
【发布时间】:2012-03-07 10:48:57
【问题描述】:

我读过 Java 中的“volatile”允许不同的线程访问同一个字段并查看其他线程对该字段所做的更改。如果是这种情况,我预测当第一个和第二个线程完全运行时,“d”的值将增加到 4。但是,每个线程将“d”的值增加到 2。

public class VolatileExample extends Thread {
private int countDown = 2;
private volatile int d = 0; 

    public VolatileExample(String name) {
        super(name);
        start();
    }

    public String toString() {
       return super.getName() + ": countDown " + countDown;
    }

public void run() {
        while(true) {
        d = d + 1;
    System.out.println(this + ". Value of d is " + d);
    if(--countDown == 0) return;
    }
}

public static void main(String[] args) {
    new VolatileExample("first thread");
    new VolatileExample("second thread");
    }
}

运行这个程序的结果是:

第一个线程:countDown 2. d 的值为 1

第二个线程:countDown 2。d的值为1

第一个线程:countDown 1。d的值为2

第二个线程:countDown 1。d的值为2

我知道如果我在程序中添加关键字“静态”, (即“private static volatile int d = 0;”),“d”将增加到 4。 我知道这是因为 d 将成为整个类共享的变量,而不是每个实例都获得一个副本。

结果如下:

第一个线程:countDown 2. d 的值为 1

第一个线程:countDown 1。d的值为3

第二个线程:countDown 2。d的值为2

第二个线程:countDown 1。d的值为4

我的问题是,如果 volatile 应该允许在两个线程之间共享“d”,为什么“private volatile int d = 0;”不会产生类似的结果?也就是说,如果第一个线程将 d 的值更新为 1,那么为什么第二个线程不将 d 的值抓取为 1 而不是 0?

【问题讨论】:

  • 感谢那个人(我认为帖子被删除了)表明这是原子性问题。
  • 这是我删除您所说的第一个答案。我删除了它,因为在重新阅读您的问题后,我认为这里的问题比简单的“您需要同步”要复杂一些。所以我在下面写了新的答案,专注于我认为真正的问题(理解实例与静态字段、线程和易失性),并简单地提到同步。 :)

标签: java multithreading volatile


【解决方案1】:

volatile 不会“允许共享”任何东西。它只是防止变量被缓存到线程本地,以便立即发生对变量值的更改。您的 d 变量是一个实例变量,因此归拥有它的实例所有。您需要重新阅读线程教程以重新调整您的假设。

一个不错的参考是here

【讨论】:

    【解决方案2】:

    这里有几个误解。您似乎没有正确理解什么是线程、什么是实例字段以及什么是静态字段。

    实例字段是在您实例化一个类后分配的内存位置(即,当您VolatileExample v = new VolatileExample() 时为字段d 分配内存位置)。要从类中引用该内存位置,请执行this.d(然后您可以写入和读取该内存位置)。要从类外部引用该内存位置,它必须是可访问的(即,不是private),然后您可以使用v.d。如您所见,类的每个实例都为其自己的字段d 获取自己的内存位置。因此,如果您有 2 个不同的 VolatileExample 实例,每个实例都有自己的独立字段 d

    静态字段是一个内存位置,一旦一个类被初始化就会被分配(忘记使用多个ClassLoaders 的可能性,只发生一次)。因此,您可以认为静态字段是某种全局变量。要引用该内存位置,您可以使用VolatileExample.d(可访问性也适用(即,如果它是private,则只能在类内完成)。

    最后,执行线程是由 JVM 执行的一系列步骤。你千万不要把线程看作一个类,或者Thread类的一个实例,它只会让你感到困惑。就这么简单:一系列步骤。

    main 步骤序列是在main(...) 方法中定义的。当您启动程序时,JVM 将开始执行这一系列步骤。

    如果您想启动一个新的执行线程以同时运行(即,您希望同时运行一个单独的步骤序列),在 Java 中您可以通过创建类 Thread 的实例并调用其start() 方法。

    让我们稍微修改一下你的代码,以便更容易理解发生了什么:

    public class VolatileExample extends Thread {
      private int countDown = 2;
      private volatile int d = 0;
    
      public VolatileExample(String name) {
        super(name);
      }
    
      public String toString() {
        return super.getName() + ": countDown " + countDown;
      }
    
      public void run() {
        while(true) {
          d = d + 1;
          System.out.println(this + ". Value of d is " + d);
          if(--countDown == 0) return;
        }
      }
    
      public static void main(String[] args) {
        VolatileExample ve1 = new VolatileExample("first thread");
        ve1.start();
        VolatileExample ve2 = new VolatileExample("second thread");
        ve2.start();
      }
    }
    

    VolatileExample ve1 = new VolatileExample("first thread"); 行创建了VolatileExample 的一个实例。这将分配一些内存位置:countdown 为 4 个字节,d 为 4 个字节。然后你开始一个新的执行线程:ve1.start();。该执行线程将访问(读取和写入)本段之前描述的内存位置。

    下一行 VolatileExample ve2 = new VolatileExample("second thread"); 创建另一个 VolatileExample 实例,它将分配 2 个新内存位置:4 个字节用于 ve2 的 countdown 和 4 个字节用于 ve2 的 d。然后,您启动一​​个执行线程,它将访问这些新的内存位置,而不是前一段中描述的那些。

    现在,无论有没有volatile,您都会看到您有两个不同的字段d:每个线程都在不同的字段上运行。 因此,您期望 d 会增加到 4 是不合理的,因为没有单个 d

    如果您将d 设为静态字段,那么只有两个线程才会(假设)在相同的内存位置上运行。只有这样volatile 才会发挥作用,因为只有这样你才能在不同线程之间共享一个内存位置。

    如果您创建一个字段volatile,则可以保证写入直接进入主内存,而读取直接来自主内存(即,它们不会被缓存在一些——非常快的——处理器上——本地缓存,操作会花费更长的时间,但会保证对其他线程可见)。

    但是,它不能保证您会看到存储在 d 上的值 4。那是因为volatile 解决了可见性问题,而不是原子性问题:increment = read from main memory + operation on the value + write to main memory。如您所见,2 个不同的线程可以读取初始值 (0),对其进行操作(本地)(获取 1),然后将其写入主存储器(最终都将写入 1)——2 个增量将是被认为只有 1 个。

    要解决这个问题,您必须使增量成为原子操作。为此,您需要使用同步机制——互斥体(synchronized (...) { ... },或显式锁)——或专门为此设计的类:AtomicInteger

    【讨论】:

    • 感谢您写得非常好。您的回答对于解释一些我不清楚的概念非常有帮助。总而言之,要使 d 始终递增到 4,您应该使用“同步(this)”之类的东西在运行中放置静态和同步方法?此外,当我阅读这篇 [文章] (javamex.com/tutorials/…) 时,似乎暗示两个不同的线程正在访问同一个变量。所以我对作者的意思感到困惑。
    • "让 d 持续递增到 4 [...] ?" 是的,就是这样。或者你可以用private static final AtomicInteger d = new AtomicInteger(0);替换d,然后用d.incrementAndGet()递增,这是一个原子递增操作(作为练习:看看JavaDocs for AtomicInteger,并尝试实现你自己的类版本-- 你可能最终会使用很多synchronized;AtomicInteger 的实际实现没有使用同步,它使用非阻塞同步,这是一个非常有趣的主题!寻找compare-and-set)。
    • 关于你提到的那篇文章,我会说你最好远离它!我已经阅读了您链接的页面,以及从那里链接的页面。它非常不精确(在讨论多线程概念时绝对需要精确),作者似乎不明白他在说什么。如果你想要一篇关于多线程的好文章,试试经典的“Java Concurrency in Practice”:这门语言不是很难而且非常精确,它有很多例子,真实世界的例子(不是class Animal, class Dog extends Animal 那种废话)。
    【解决方案3】:

    volatile 可以使共享安全(如果单个读取或写入操作的原子性就足够了),它不会导致共享。

    注意,如果你把dstatic设为d,实际上并不确定d会有什么值,因为语句d = d + 1不是原子的,即一个线程可能在读取和写入d之间被中断.同步块或AtomicInteger 是典型的解决方案。

    【讨论】:

      猜你喜欢
      • 2013-04-14
      • 2013-04-23
      • 2018-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-08-26
      • 1970-01-01
      相关资源
      最近更新 更多