10

假设:

  1. 只有一个特定的线程会设置某个引用字段(不是 long 或 double,所以写入它是原子的)
  2. 有任意数量的线程可能会读取相同的字段
  3. 稍微陈旧的读取是可以接受的(最多几秒钟)

在这种情况下,您是否需要 volatile 或 AtomicReference 或类似的东西?

这篇文章指出:

如果您严格遵守单写原则,则不需要内存屏障。

这似乎表明在我所描述的情况下,你真的不需要做任何特别的事情。

所以,这是我运行的一个测试,结果很奇怪:

import org.junit.Test;

public class ThreadTest {
    int onlyWrittenByMain = 0;
    int onlyWrittenByThread = 0;

    @Test
    public void testThread() throws InterruptedException {
        Thread newThread = new Thread(new Runnable() {
            @Override
            public void run() {
                do {
                    onlyWrittenByThread++;
                } while (onlyWrittenByMain < 10 || onlyWrittenByThread < 10);
                System.out.println("thread done");
            }
        });
        newThread.start();

        do {
            onlyWrittenByMain++;
            // Thread.yield();
            // System.out.println("test");
            // new Random().nextInt();
        } while (onlyWrittenByThread < 10);
        System.out.println("main done");
    }
}

有时运行它会输出“线程完成”,然后永远挂起。有时它确实完成了。所以线程看到了主线程所做的更改,但显然 main 并不总是看到线程所做的更改?

如果我将系统放入,或者 Thread.yield,或者随机调用,或者使 onlyWrittenByThread 不稳定,它每次都会完成(尝试大约 10 次以上)。

这是否意味着我上面引用的博客文章不正确?即使在单一编写器场景中,您也必须有一个内存屏障?

没有人完全回答这个问题,所以我想我猜可能不需要内存屏障是正确的,但是如果没有创建之前发生关系的东西,java 编译器和热点可以进行优化(例如提升)会让它不为所欲为。

4

4 回答 4

5

问题是在多核系统上缓存 - 没有像 volatile 这样的强制发生之前的关系(内存屏障的东西),你可以让你的写入线程写入其核心缓存中的变量副本,而你的所有读取线程读取另一个另一个核心上的变量副本。另一个问题是原子性,另一个答案解决了这个问题。

于 2013-06-07T20:04:19.260 回答
2

代码中的主要问题不在于 CPU 将做什么,而是 JVM 将如何处理它:变量提升的风险很高。这意味着 JMM(Java 内存模型)允许 JVM 重写:

public void run() {
    do {
        onlyWrittenByThread++;
    } while (onlyWrittenByMain < 10 || onlyWrittenByThread < 10);
    System.out.println("thread done");
}

作为另一段代码(注意局部变量):

public void run() {
    int localA = onlyWrittenByMain;
    int localB = onlyWrittenByThread;
    do {
        localB ++;
    } while (localA < 10 || localB < 10);
    System.out.println("thread done");
}

碰巧这是hotpost做出的一个相当普遍的优化。在您的情况下,一旦进行了优化(当您调用该方法时可能不是立即,而是在几毫秒之后),您在其他线程中所做的任何事情都不会从该线程中看到。

于 2013-06-07T22:37:27.623 回答
1

我猜你误解了你从马丁的博客中引用的话。在 x86/64 硬件上,您可以使用 Atomic*.lazySet 获得比 'volatile' 更高的性能,因为它提供了比 store-load barrier 便宜得多的 store-store barrier。据我了解,您应该改用 AtomicLong 并使用lazySet 来保证代码不会重新排序。

更多详情请参考这篇文章:http: //psy-lob-saw.blogspot.com/2012/12/atomiclazyset-is-performance-win-for.html

于 2013-10-31T02:59:06.737 回答
1

它是必需的,因为读取字段上的更改可能对读取线程不可见。您应该创建一个发生在关系之前。

于 2013-06-07T18:53:34.223 回答