17

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,所以它永远循环。(顺便说一句,该代码在没有volatileJRE7 的情况下终止。)

现在考虑这个类:

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

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

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

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

4

3 回答 3

11

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

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

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

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

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

于 2012-06-12T12:37:49.040 回答
4

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

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

有关特定的 JLS 参考,请参阅发生之前部分(以及页面的其余部分)。

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

于 2012-06-12T12:39:57.173 回答
4

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

顺便说一句,该代码在 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 都会看到写入 w 到 v 的值,使得:

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

于 2012-06-12T13:35:00.353 回答