代码之家  ›  专栏  ›  技术社区  ›  stonar96

Java发生在侦听器实现的关系之前

  •  0
  • stonar96  · 技术社区  · 2 年前

    考虑Java应用程序的以下部分

    final List<String> strings = new CopyOnWriteArrayList<>();
    volatile Consumer<? super String> listener;
    
    void add(String s) {
        strings.add(s);
        Consumer<? super String> l = listener;
        if (l != null) {
            l.accept(s);
        }
    }
    

    这个 add(String s) 方法可以从多个线程调用并添加 s 到 CopyOnWriteArrayList (也可以是任何其他线程安全的 Collection )如果有 listener ,使用调用 s 作为论据。请注意 听众 是 volatile .

    在某个时间点,任何线程都会调用以下内容 setupListener() 方法一次。

    void setupListener() {
        listener = System.out::println;
        strings.forEach(System.out::println);
    }
    

    这个 setupListener() 方法设置 听众 并且还处理以前错过的对 听众 通过迭代添加的 strings .

    我的问题是,假设 setupListener() 方法在某个时间点被调用一次,使用此代码是否可能会错过 串 条目,即 串 参赛作品可能因比赛情况而无法打印?

    注意,我知道 串 在比赛条件下,条目可能会打印两次(或者不按特定顺序)。这是允许的,我不在乎。重要的是不要错过任何一个。还要注意,这个问题与同步或其他类型的锁定无关,我不能更改上层代码,也不会调用它 听众 .

    关于这个问题,我认为没有 串 条目将由于以下原因而丢失:假设 String s 已错过。这意味着 听众 被读取为 null 在中 add 方法,否则就会打印出来。然而,由于

    • 添加 s 到 串 发生在阅读之前 听众 像 无效的
    • 和阅读 听众 像 无效的 在设置之前发生 听众 在中 setupListener 方法
    • 并设置 听众 在中 setupListener 方法发生在迭代并打印之前 串 ,

    s 在迭代时必须打印 串 自从 串 包含 s 。这违反了以下假设 s 已错过。

    这个论点正确吗?

    1 回复  |  直到 2 年前
        1
  •  1
  •   rzwitserloot    2 年前

    我很确定你是安全的。但你的论点遗漏了一些重要的细节。

    你的陈述有问题:

    到 CopyOnWriteArrayList (也可以是任何其他线程安全集合)

    这表明人们对事物的运作方式存在误解。 线程安全不是非黑即白的情况 。不能说“此集合是线程安全的”和“此集合不是线程安全的。”。线程安全不在于对象/类,而在于操作。有些操作可以是线程安全的,而其他操作则不是。例如,琐碎的是:

    if (!map.containsKey(key)) {
      map.put(key, calculate(key));
    }
    

    不是线程安全的。无论您的底层线程如何安全 map 出于显而易见的原因,引用是。

    COWList的“线程安全”是在另一个线程迭代列表时修改列表,这是由于它的基本属性(因此,“写时复制”部分,确保所有迭代器都不受其底层列表被修改的影响,因为这永远不会发生)。同样是线程安全的是两个线程试图同时添加到它。

    这个 add() COWList的代码专门获取COWList实例内部的锁。直接来源:

            synchronized (lock) {
                Object[] es = getArray();
                int len = es.length;
                es = Arrays.copyOf(es, len + 1);
                es[len] = e;
                setArray(es);
                return true;
            }
    

    在一个同步块退出和另一个线程稍后进入同一个锁对象之间建立了Happens Before关系(HB是 必修的 对于代码a对字段所做的任何改变,代码B肯定是可观察到的;如果没有HB关系,JVM可以自由地使该字段写入可见 是否 -任何一种行为都是可以接受的,因此,如果您的代码依赖于此,那么您就编写了一个bug。一个讨厌的,非常非常难测试的!)

    有了锁,“呼叫”的行为 add “来自2个线程”是线程安全的。

    调查时有点棘手 iterator() ,这就是什么 for (String s : strings) 最终呼叫:

        public Iterator<E> iterator() {
            return new COWIterator<E>(getArray(), 0);
        }
        /** The array, accessed only via getArray/setArray. */
        private transient volatile Object[] array;
    
        /**
         * Gets the array.  Non-private so as to also be accessible
         * from CopyOnWriteArraySet class.
         */
        final Object[] getArray() {
            return array;
        }
    

    哦!未锁定 lock ,因此,由 添加 根本不交互 通过获得迭代器的过程。你得不到的好处 synchronized 除了与其他线程的交互 而且 同步的 在同一件事上。这个 volatile 在数组上是保存您的方法,这只是因为COWList专门从不写入数组(它复制整个数组并更新引用,而且 参考 那就是 不稳定的 ,而不是数组内容本身)。

    不稳定的 也建立了Happens Before。