【问题标题】:jvm reordering/visibility effect testjvm重新排序/可见性效果测试
【发布时间】:2011-06-07 11:24:03
【问题描述】:

在编写一些 java 文章时,我试图重现 re-ordering,以防多线程环境中的对象构造不同步。 当构造一个重对象时没有同步/易失性/finals 并且其他线程在构造函数调用后立即访问它的情况。 这是我尝试的代码:

public class ReorderingTest {
    static SomeObject<JPanel>[] sharedArray = new SomeObject[100];

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            String name = "watcher" + i;
            new Thread(new Watcher(name)).start();
            System.out.printf("watcher %s started!%n", name);
        }
    }

    static class Watcher implements Runnable {
        private String name;

        Watcher(String name) {
            this.name = name;
        }

        public void run() {
            while (true) {
                int randomIndex = (int) (Math.random() * sharedArray.length);
                SomeObject<JPanel> item = sharedArray[randomIndex];
                if (item == null) {
                    //System.out.printf("sharedArray[%s]=null%n", randomIndex);
                    double r = 1 + Math.random() * 1000;
                    sharedArray[randomIndex] = new SomeObject<JPanel>(
                            new JPanel(), UUID.randomUUID().toString(), r, (float)r * 33, (long)r);
                } else {
                    //System.out.printf("sharedArray[%s]=<obj>!%n", randomIndex);
                    if (item.value == null ||
                            (item.stringField == null) ||
                            (item.doubleField == 0) ||
                            (item.floatField == 0) ||
                            (item.longField == 0)
                            ) {
                        System.err.printf("watcher %s sees default values: %s!%n", name, item);
                    } else {
                        // fully initialized! run new construction process
                        double r = 1 + Math.random() * 1000;
                        sharedArray[randomIndex] = new SomeObject<JPanel>(
                                new JPanel(), UUID.randomUUID().toString(), r, (float)r * 37, (long)r);

                    }
                }
                /*try {
                    TimeUnit.NANOSECONDS.sleep(randomIndex);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }*/
            }
        }
    }

    static class SomeObject<V> {
        V value;
        String stringField;
        double doubleField;
        float floatField;
        long longField;

        SomeObject(V value, String stringField, double doubleField, float floatField, long longField) {
            this.value = value;
            this.stringField = stringField;
            this.doubleField = doubleField;
            this.floatField = floatField;
            this.longField = longField;
        }

        @Override
        public String toString() {
            return "SomeObject{" +
                    "value=" + value == null ? "null" : "<obj>" +
                    ", stringField='" + stringField + '\'' +
                    ", doubleField=" + doubleField +
                    ", floatField=" + floatField +
                    ", longField=" + longField +
                    '}';
        }
    }
} 

-但到目前为止没有任何效果,我已经在不同的 2,4 和 8 核 Intel/AMD PC 上使用 windows 进行了测试,运行了几个小时 - 没有重新排序效果 - System.err.printf("watcher %s看到 ...") - 未调用,静态 sharedArray[randomIndex] 引用始终包含完全构造的值。

怎么了?如何重现这个?

【问题讨论】:

  • 很难刻意复制。您通常需要您的代码在多个 cpu 上运行并被 hoptspot 视为“热”,以便 jit 积极优化它。即便如此,它仍然很难在测试中重现。
  • according to jmm:“有许多潜在的重新排序来源,例如编译器、JIT 和缓存” - 这种效果还有 2 个其他可能性:编译器和缓存(硬件)......但似乎它们在家庭条件下也非常罕见。

标签: java multithreading concurrency memory-visibility


【解决方案1】:

这是一篇不错的文章,应该显示在 x86 上重新排序(相当了不起,因为 x86 内存模型非常“安全”):

http://bartoszmilewski.wordpress.com/2008/11/05/who-ordered-memory-fences-on-an-x86/

您的示例不会显示重新排序。编译器不会重新排序以“在分配之后但在构造之前存储对象引用”,因为构造函数可能会抛出,因此需要恢复引用。由于给定的保证,某些处理器可能会重新排序,但不兼容英特尔。

【讨论】:

  • 谢谢,似乎最合理。但他们在jmm-faq FinalFieldExample 中将其作为非最终字段的可见性问题(重新排序)的示例。
  • 顺便说一句,在 Solaris sparc (SunOS 5.10 sun4us sparc) 上测试 - 也没有变化,似乎这里也禁止这种原始的重新排序。
  • 错误。请参阅stackoverflow.com/questions/16178020/… 或通过 x86 你不包括 x64 吗?
【解决方案2】:

不保证重新订购。它通常仅在 JIT 确定可能有性能提升时发生。

在你的情况下,你正在寻找的不是重新排序,而是一个似乎没有被正确初始化的对象。

您的对象很可能每次都使用新的内存位置(或至少一个不在缓存中的位置)在 x86/x64 架构上,我发现缓存在第一次加载时总是正确的内存何时被另一个线程更新。

【讨论】:

  • 为什么“对象很可能每次都使用新的内存位置”——因为它们很大?你的意思是在 PC 上的 ccNuma 架构中有一些功能可以确保在 jvm 中正确初始化对象而无需安全发布?
  • 对象分配在伊甸园空间。如果您的 Eden 空间小到足以放入缓存中,那么在另一个线程中创建新对象后,您可以在一个线程的缓存中保存旧数据。但是,由于每个线程都在创建大量对象,因此它们会确保推出任何旧数据。
  • 那么,只有很少创建小对象并且拥有小的 Eden 大小 - 可以重现这个吗?
  • 如果线程数与内核数一样多,并且某个线程仅检查对象,则更有可能发现问题。
【解决方案3】:

重新排序不一定会发生。当它发生时,很难观察到。你100 的样本量太小了。首先尝试十亿。

假设发生重新排序,在写入时,首先分配引用,然后填充字段。您的读取过程首先读取引用,然后读取字段。所以 write 和 read 遵循相同的顺序,read 几乎不可能赶上并领先于 write。

您可以通过以下方式解决该问题

  1. 在构造函数中暂停(因此在写入中暂停)
  2. 以相反方向写入和读取字段

不过,重新排序可能不会发生,您将无法观察到它。

【讨论】:

  • 谢谢,试过了。构造函数中的暂停 (TimeUnit.NANOSECONDS.sleep((long)Math.random() * 1000)) + 更小的 sharedArray 大小 [10] + 更多线程(3000,如果更多,jmv 会因 OOM 崩溃) - 没有帮助,没有变化。
  • 更有可能总是在添加引用之前分配字段,但其他 CPU 以不同的顺序看到它,例如:银行缓存,如在某些 Alpha 机器上。
【解决方案4】:

您可以尝试将sharedArray 声明为非易失性吗?在我看来,将 volatile 引用传递给正确构造的对象是安全的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-21
    • 2022-08-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-19
    相关资源
    最近更新 更多