在两个线程之间使用 LinkedBlockingQueue 是不是意味着我们不需要同步它们对共享数据的访问?

Posted

技术标签:

【中文标题】在两个线程之间使用 LinkedBlockingQueue 是不是意味着我们不需要同步它们对共享数据的访问?【英文标题】:Does using a LinkedBlockingQueue between two threads mean we don't need to synchronize their access to shared data?在两个线程之间使用 LinkedBlockingQueue 是否意味着我们不需要同步它们对共享数据的访问? 【发布时间】:2020-07-31 17:21:42 【问题描述】:

我有一种情况,我正在维护的项目包含一段代码,其中:

    线程T1 定期更新List<String> 类型的字段,然后将值添加到LinkedBlockingQueue

    // ctor
    List<String> list = null;
    queue = new LinkedBlockingQueue(); // Integer.MAX_VALUE capacity
    
    // Thread T1
    list = getNewList();
    queue.offer(/* some value */);
    

    另一个线程T2 定期轮询queue,并在收到某个值后读取list

    // Thread T2
    Object value = queue.poll();
    if (Objects.equals(value, /* some desired value */) 
        // Read `list` in (1)
    
    

引起我注意的是两个线程在访问list(也没有标记volatile)时缺乏同步,这让我认为该代码中可能存在数据竞争。但是,当我提到BlockingQueue时,我发现有一句话说:

内存一致性影响:与其他并发集合一样,线程中的操作在将对象放入 BlockingQueue 之前发生在另一个线程中从 BlockingQueue 中访问或删除该元素之后的操作。

这是否意味着T2 始终可以保证观察到T1list 所做的更改,并且不会出现数据竞争?

【问题讨论】:

是的,它由 javadoc 中所述的 java 内存模型保证 第一步只发生一次? 在线程之间共享不受保护的列表不是一个好主意。 【参考方案1】:

这是一个更普遍的效果的特定实例described here(该页面上的memory consistency errors link特别值得一读。

所有这些集合通过定义将对象添加到集合的操作与访问或删除该对象的后续操作之间的发生前关系来帮助避免内存一致性错误。

所以,更一般地说,当您有两段这样的代码时:

// list is a non thread safe ArrayList
list.add("abcd");
// col is *any* java.util.concurrent collection
col.add(anObject); // or put, or offer...

if (col.get() == anObject)  …  // or poll etc.

如果第二段代码由第一段以外的其他线程执行(并且条件为真),则在检索到anObject 之后放置的任何代码都可以看到添加到列表中的“abcd”。

但是:

它必须是同一个对象(anObject,你从并发集合中放置/获取的东西) 假设在col.addObject(anObject) 调用之后,列表没有进一步修改 它(显然,但值得注意)必须已实际放入集合中。 offer() 可能在另一个线程中返回 false,poll() 可能在这个线程中超时。

【讨论】:

col.get(anObject); 非常具有误导性,因为通常您不会告诉查询方法要返回什么。由于您正确命名了有关返回相同对象的查询的约束,因此使用if(col.get() == anObject) … 来演示唯一有效的用例是有意义的。还值得补充的是,它仅在 anObject 从未添加过一次以上且在 col.add(anObject); 操作之后不会对 list 或其元素进行任何修改时才有效。 感谢@Holger,更新了答案。对列表元素之一的修改将如何在这里发挥作用? (评论中的“...或其元素”部分) @pafauk。保证是在col.add 之前的T1 中的任何写入在col.get 之后的T2 中都是可见的。但是,如果 T1 在col.add 之后继续写入list 或修改list 内的对象(例如list.get(0).setTitle("newTitle")),那么直到T1 和T2 两者执行另一个col.add/col.get(或类似的发生前边界) 人们倾向于一概而论,所以如果你有一个列表,而不是像字符串这样的不可变类型,对包含的元素的修改与对列表的修改遵循相同的规则。 我以为您是在说使用列表元素 other 而不是 anObject 会以某种方式混淆存储/检索之前所做更改的可见性。【参考方案2】:

这是否意味着 T2 始终可以保证观察到 T1 对 list 所做的更改,并且不会出现数据竞争?

您受到内存模型的保护,即在添加到队列之前对列表所做的更改将在第二个线程看到它们之前对第二个线程可见。

否则这些队列不会很有用。

但对于以后所做的任何更改。所有赌注都取消了。

定期更新

您说:T1 会定期更新 List 类型的字段,然后将其添加到队列中。

如果按字段,您的意思是一些实例变量或静态变量。 周期性地,您的意思是在将其放入队列后从 T1 对其进行变异。

那么我会说你肯定会遇到麻烦。最好的做法是发送一份这样的列表副本:

list = getNewList();
queue.offer(new ArrayList<String>(list));

最好不要在线程之间共享对象。只需创建并发送即可。

实际上,通过队列发送共享列表是没有意义的。您不妨一直分享它(以某种受保护的方式)。

您使用队列的原因是您想向另一个线程发送消息,此时您应该释放它,并且什么都不做。

内存模型

同样,内存模型将保证对象在到达另一个线程后是好的。因此,只要 T1(或任何其他线程)不再对对象执行任何操作,可见性就不会成为问题。

活体注意事项

无论您做什么,都要小心排队“活动对象”,例如 Hibernate 控制的对象和打算存活很短时间的对象。当我使用这个特性时,我通常会发送 id 和类似的东西,这样 T2 就可以独立地从数据库中恢复对象,而不是挂在另一个线程中管理的对象上。

因此,您还需要注意列表中的内容。当然,不可变字符串是可以的。

【讨论】:

以上是关于在两个线程之间使用 LinkedBlockingQueue 是不是意味着我们不需要同步它们对共享数据的访问?的主要内容,如果未能解决你的问题,请参考以下文章

在两个线程之间共享一个 ArrayList?

两个线程之间的列表共享

在两个线程之间使用 LinkedBlockingQueue 是不是意味着我们不需要同步它们对共享数据的访问?

两个线程c ++之间的boost asio通信

当在两个布局之间快速切换使用 UISegmentedControl 时,我得到一个线程 1 exc_bad_access code=2 错误

在两个线程之间共享 QAxObject?