如果您只有一个写线程,您是否需要特殊的并发?

时间:2013-06-07 18:48:45

标签: java concurrency

假设:

  1. 只有一个特定的线程设置了某个引用字段(不是long或double,因此写入它是原子的)
  2. 可以读取相同字段的任意数量的线程
  3. 稍微陈旧的读取是可以接受的(最多几秒钟)
  4. 在这种情况下,您是否需要volatile或AtomicReference或类似的东西?

    This article州:

      

    如果严格遵守单一作者原则,则不需要内存障碍。

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

    所以,这是一个测试我跑了好奇的结果:

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

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

    如果我把系统放入,或者Thread.yield,或者随机调用,或者只使用WWWitThread使其变为易失性,它每次都会完成(尝试大约10次以上)。

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

    没有人回答这个问题,所以我想我认为不需要内存屏障是正确的,但如果没有创建事先关系的东西,java编译器和热点可以进行优化(例如。提升,这将使它不能做你想做的事。

4 个答案:

答案 0 :(得分:5)

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

答案 1 :(得分: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做出的相当普遍的优化。在你的情况下,一旦进行了优化(当你调用该方法时可能不会立即但几毫秒之后),无论你在其他线程中做什么,永远都不会从该线程中看到。

答案 2 :(得分:1)

这是必要的,因为读取线程可能看不到读取字段的更改。您应该在关系之前创建一个发生。

答案 3 :(得分:1)

我猜你误解了你引用马丁博客的话。在x86 / 64硬件上,您可以使用Atomic * .lazySet获得比“volatile”更多的性能,因为它提供的商店存储屏障比商店负载屏障便宜得多。 据我所知,您应该使用AtomicLong,并使用lazySet来保证代码不会被重新排序。

有关详细信息,请参阅此文章: http://psy-lob-saw.blogspot.com/2012/12/atomiclazyset-is-performance-win-for.html