【问题标题】:java: `volatile` private fields with getters and settersjava:带有getter和setter的`volatile`私有字段
【发布时间】:2012-06-12 12:28:34
【问题描述】:

如果在多个线程中使用实例,我们是否应该将私有字段声明为volatile

Effective Java中,有一个例子,没有volatile,代码就不能工作:

import java.util.concurrent.TimeUnit;

// Broken! - How long would you expect this program to run?
public class StopThread {
    private static boolean stopRequested; // works, if volatile is here

    public static void main(String[] args) throws InterruptedException {
        Thread backgroundThread = new Thread(new Runnable() {
            public void run() {
                int i = 0;
                while (!stopRequested)
                    i++;
            }
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

解释说

while(!stopRequested)
    i++;

被优化为这样的:

if(!stopRequested)
    while(true)
        i++;

所以后台线程看不到对stopRequested 的进一步修改,因此它永远循环。 (顺便说一句,该代码在 JRE7 上没有 volatile 的情况下终止。)

现在考虑这个类:

public class Bean {
    private boolean field = true;

    public boolean getField() {
        return field;
    }

    public void setField(boolean value) {
        field = value;
    }
}

和一个线程如下:

public class Worker implements Runnable {
    private Bean b;

    public Worker(Bean b) {
        this.b = b;
    }

    @Override
    public void run() {
        while(b.getField()) {
            System.err.println("Waiting...");
            try { Thread.sleep(1000); }
            catch(InterruptedException ie) { return; }
        }
    }
}

上面的代码在没有使用 volatile 的情况下按预期工作:

public class VolatileTest {
    public static void main(String [] args) throws Exception {
        Bean b = new Bean();

        Thread t = new Thread(new Worker(b));
        t.start();
        Thread.sleep(3000);

        b.setField(false); // stops the child thread
        System.err.println("Waiting the child thread to quit");
        t.join();
        // if the code gets, here the child thread is stopped
        // and it really gets, with JRE7, 6 with -server, -client
    }
}

我认为由于公共设置器,编译器/JVM 永远不应该优化调用 getField() 的代码,但 this article 说有一些“Volatile Bean”模式(模式 #4),应该应用创建可变线程安全类。 更新:也许那篇文章仅适用于 IBM JVM?

问题是:JLS 的哪一部分明确或隐含地说,具有公共 getter/setter 的私有原始字段必须声明为 volatile(或者它们不必)?

对不起,问题很长,我试图详细解释这个问题。如果有不清楚的地方,请告诉我。谢谢。

【问题讨论】:

  • 不需要那个字段来取消线程,你可以在线程上使用中断标志。
  • @NathanHughes,这些类只是最小的例子,实际代码不同,这里不需要线程中断。

标签: java volatile memory-model java-memory-model


【解决方案1】:

问题是:JLS 的哪一部分明确或隐含地说,必须将具有公共 getter/setter 的私有原始字段声明为 volatile(或者它们不必声明)?

JLS 内存模型不关心 getter/setter。从内存模型的角度来看,它们是无操作的——您也可以访问公共字段。将布尔值包装在方法调用后面不会影响其内存可见性。您的后一个示例完全靠运气。

如果在多个线程中使用实例化,我们是否应该将私有字段声明为 volatile?

如果要在多线程环境中使用类(bean),您必须以某种方式考虑到这一点。制作私有字段volatile 是一种方法:它确保每个线程都能看到该字段的最新值,而不是任何缓存/优化过的值。但是并没有解决atomicity的问题。

The article you linked to 适用于任何遵守 JVM 规范(JLS 所依赖的规范)的 JVM。根据 JVM 供应商、版本、标志、计算机和操作系统、运行程序的次数(HotSpot 优化通常在第 10000 次运行后启动)等,您将获得各种结果,因此您确实必须了解规范并仔细遵守以创建可靠的程序。在这种情况下进行试验是了解事情如何工作的糟糕方法,因为 JVM 可以按照它想要的任何方式运行,只要它符合规范,而且大多数 JVM 确实包含各种动态优化的负载。

【讨论】:

  • 谢谢。设置布尔变量(或除 long/double 之外的任何其他原始变量)是原子操作。我的问题是关于 JSL 在这种情况下如何定义代码行为。或者它如何没有定义,如果我的意思很清楚的话。
  • JLS 没有针对您的情况定义任何特定内容。 bean 只是方法调用后面的变量,memory model 按原样适用。从内存模型的角度来看,方法调用是无操作的。通过原子性,我的意思是你的 bean 可能最终处于不一致的状态,如果你有逻辑上相互依赖的字段:例如,“开始”时间应该在“结束”时间之前或类似的时间 - 除非你有,否则不能保证一种以原子方式更改这两个字段的机制。
  • 顺便说一下,volatile longdouble 值的写入和读取始终是原子的。请参阅 JLS 17.7。
  • @John Vint:从 17.7 开始:Writes and reads of volatile long and double values are always atomic. 这不是特定于实现的。特定于实现的是 non-volatile 64 位值的写入和读取。
  • 哦,你是对的,对不起。我想念你的声明,读成By the way writes and reads of long and double values are always atomic
【解决方案2】:

在我回答你的问题之前,我想先解决

顺便说一句,该代码在 JRE7 上没有 volatile 终止

如果您使用不同的运行时参数部署相同的应用程序,这可能会发生变化。提升不一定是 JVM 的默认实现,因此它可以在一个而不是另一个中工作。

要回答您的问题,没有什么可以阻止 Java 编译器像这样执行您的后一个示例

@Override
public void run() {
    if(b.getField()){
        while(true) {
            System.err.println("Waiting...");
            try { Thread.sleep(1000); }
            catch(InterruptedException ie) { return; }
        }
    }
}

它仍然是顺序一致的,因此维护了 Java 的保证 - 你可以具体阅读17.4.3

在每个线程 t 执行的所有线程间动作中, t 的程序顺序是一个总顺序,它反映了其中的顺序 这些动作将根据线程内执行 t的语义。

如果所有动作都发生在一个动作中,则一组动作是顺序一致的 与程序一致的总顺序(执行顺序) 顺序,此外,变量 v 的每次读取 r 都会看到该值 由 write w to v 这样写:

换句话说 - 只要一个线程以相同的顺序看到一个字段的读取和写入,而不管编译器/内存重新排序如何,它就被认为是顺序一致的。

【讨论】:

    【解决方案3】:

    不,该代码同样不正确。 JLS 中没有规定字段必须 声明为 volatile。但是,如果您希望您的代码在多线程环境中正常工作,那么您必须遵守可见性规则。 volatile 和 synchronized 是正确使数据跨线程可见的两个主要工具。

    就您的示例而言,编写多线程代码的困难在于许多形式的错误代码在测试中都能正常工作。仅仅因为多线程测试在测试中“成功”并不意味着它是正确的代码。

    有关具体的 JLS 参考,请参阅Happens Before 部分(以及页面的其余部分)。

    请注意,作为一般经验法则,如果您认为自己想出了一个巧妙的新方法来绕过“标准”线程安全的习惯用法,那么您很可能是错误的。

    【讨论】:

    • 如果 JSL 中没有说明该字段必须声明为 volatile,您怎么知道它必须声明为 volatile?您对测试多线程代码的看法非常好,但问题是关于 JSL。
    • @khachik - JLS 提供了使代码线程安全必须做的具体细节。我引用的部分中有多种选择。 JLS 中的任何内容都要求您使代码线程安全,因此您不必使字段易失(否则默认情况下所有字段都是易失的)。
    • 当然。它没有回答我的问题。
    • @khachik - 也许你可以解释一下它没有回答你问题的哪一部分?
    • “那么你必须遵守可见性规则”——JLS 的哪一部分定义了这些规则?您发布的链接不包含它。如果 JLS 中没有明确定义该行为,那么 JLS 的哪一部分暗示了它?我的问题清楚了吗?
    猜你喜欢
    • 2019-03-01
    • 1970-01-01
    • 2014-03-27
    • 2013-06-22
    • 1970-01-01
    • 2020-02-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多