java:具有 getter 和 setter 的“易失性”私有字段

我们是否应该声明私有字段,就好像实例化在多个线程中使用一样?volatile

Effective Java 中,有一个示例,其中代码在没有易失性的情况下无法正常工作:

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++;

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

现在考虑以下类:

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

上面的代码在不使用易失性的情况下按预期工作:

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

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

问题是:JLS的哪一部分显式或隐式地表示具有公共getter/setter的私有基元字段必须声明为(或者它们不必)?volatile

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


答案 1

问题是:JLS的哪个部分明确或隐式地表示具有公共getter/setter的私有基元字段必须声明为易失性(或者它们不必)?

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

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

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

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


答案 2

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

顺便说一句,该代码在JRE7上终止时没有易失性

如果要使用不同的运行时参数部署同一应用程序,则此情况可能会更改。提升不一定是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 都会看到写入 w 到 v 的值,使得:

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


推荐