Java监听器实现的happens-before关系

问题描述 投票:0回答:1

考虑 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调用它
作为参数。请注意,
listener
volatile

在某个时间点,任何线程都会调用以下

setupListener()
方法一次。

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

setupListener()
方法设置
listener
并通过迭代添加的
listener
来处理之前错过的
strings
调用。

我的问题是,假设

setupListener()
方法在某个时间点被调用一次,这段代码是否有可能错过
strings
条目,即
strings
条目可能不会被打印?

请注意,我知道

strings
条目可能会在出现竞争条件时打印两次。这是允许的,我不在乎。重要的是不要错过任何一个。另请注意,这个问题与同步或其他类型的锁定无关,我无法更改上面的代码。

关于这个问题,我认为不会遗漏任何

strings
条目,原因如下:假设遗漏了
String s
。这意味着
listener
null
方法中被读作
add
,否则它会被打印出来。然而,自从

  • s
    添加到
    strings
    发生在将
    listener
    读取为
    null
  • 之前
  • 并且将
    listener
    读取为
    null
    发生在在
    listener
    方法中设置
    setupListener
    之前
  • 并在
    listener
    方法中设置
    setupListener
    发生在迭代并打印
    strings
    ,
  • 之前
迭代

s

 时必须打印 
strings
,因为
strings
包含
s
。这违反了
s
被遗漏的假设。

这个说法正确吗?

java multithreading listener race-condition happens-before
1个回答
0
投票

我很确定你很安全。但你的论点缺少一些重要的细节。

你的陈述有问题:

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

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

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

不是线程安全的。无论您的底层

map
引用有多线程安全,原因显而易见。

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

COWList 的

add()
代码专门获取 COWList 实例内部的锁。直接从源头看:

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

在退出的同步块和稍后在同一个锁对象上进入一个同步块之间建立发生-之前关系(HB 是代码 A 对字段所做的任何更改必须由代码 B 观察到的条件) ;如果没有 HB 关系,JVM 可以自由地使该字段写入可见或不 - 任何一种行为都是可以接受的,因此,如果您的代码依赖于此,那么您就编写了一个错误。一个令人讨厌的、非常难以测试的错误!) 锁定到位后,“从 2 个线程调用

add

”的行为就是线程安全的。

调查 

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

没有锁定,因此,

add
建立的任何HB都不会与获取迭代器的过程发生交互
。除了与同一事物上的其他线程交互之外,您无法获得 synchronized 的好处。数组上的
synchronized
可以拯救你,只是因为 COWList 从不写入数组(它复制整个数组并更新引用,而 reference 就是
volatile
,而不是数组内容本身)。
volatile 还建立了 Happens-Before。
    

© www.soinside.com 2019 - 2024. All rights reserved.