当前位置: 首页 > 知识库问答 >
问题:

LinkedBlockingQueue中的addAll()是线程安全的(如果不是,也是解决方案)?

聂宜
2023-03-14

引用文档:

"BlockingQueue实现是线程安全的。所有排队方法都使用内部锁或其他形式的并发控制以原子方式实现它们的效果。但是,除非在实现中另有说明,否则批量集合操作addAll、perspsAll、retainAll和removeAll不一定以原子方式执行。因此,例如,addAll(c)在只添加c中的一些元素后可能会失败(引发异常)。"

由于在LinkedBlockingQueue.addAll()操作的描述中没有写任何特别的东西,我不得不假设这不是线程安全的。

您是否同意我的观点,即为了保证通过 addAll() 添加的所有元素都是连续的(即一起添加),唯一的解决方案是每次修改队列(使用添加接受操作)时使用 Lock?例如:

BlockingQueue<T> queue = new LinkedBlockingQueue<>();
Lock lock = new ReentrantLock();

//somewhere, some thread...
lock.lock();
queue.addAll(someCollection);
lock.unlock();

//somewhere else, (maybe) some other thread...
lock.lock();
queue.take();
lock.unlock();

重要更新:哇,没有人在前面的例子中看到一个大错误:由于<code>take()</code>是一个阻塞操作,并且由于需要锁才能将元素添加到队列中,一旦队列为空,程序将进入死锁状态:由于锁被<code<take(()</code>所拥有,所以写入程序无法写入,同时,take()将处于阻塞状态,直到队列中写入了某些内容(由于前面的原因,这不会发生)。有什么想法吗?我认为最明显的一个是移除take()周围的锁,但可能无法保证addAll()所需的原子性。

共有2个答案

狄雅珺
2023-03-14

我相信你混淆了线程安全与原子。据我所知,批量操作是线程安全的,尽管不是原子的。

我不认为你需要使用外部重入锁来使你的阻塞队列是线程安全的。实际上,addAll() 是通过迭代给定的集合并在队列上为集合的每个元素调用 add() 来实现的。由于 add() 是线程安全的,因此您不需要同步任何内容。

当javadocs这样说时:

因此,举例来说,addAll(c)在只添加了c中的一些元素后可能会失败(抛出异常)

这意味着当< code>addAll(c)返回时,可能只添加了给定集合< code>c的某些元素。但是,这并不意味着您需要锁定队列来调用< code>take()或任何其他操作。

编辑:

根据您的用例,您可以按照您的建议使用锁,不过我会将其设置为<code>BlockingQueue</code>实现的内部,这样调用方类就不需要遵循lock/call_some_method_from_the_queue/unlock模式。在使用锁时,我会更加小心,即在<code>try/finally</code>块中使用它:

public class MyQueue<T> extends LinkedBlockingQueue<T> {

    private final Lock lock = new ReentrantLock();

    @Override
    public boolean addAll(Collection<T> c) {
        boolean r = false;
        try {
            this.lock.lock();
            r = super.addAll(c);
        } finally {
            this.lock.unlock();
        }
        return r;
    }

    @Override
    public void add(T e) {
        try {
            this.lock.lock();
            super.add(e);
        } finally {
            this.lock.unlock();
        }
    }

    // You don't need to lock on take(), since
    // it preserves the order in which elements
    // are inserted and is already thread-safe
}
李良策
2023-03-14

addAll仍然是线程安全的,它只是不提供原子性。所以这取决于你的用例/期望是什么。

如果在没有显式锁定的情况下使用addAll,那么如果其他线程试图写入队列(添加新元素),则所添加元素的顺序没有保证,它们可能会混淆。如果这是一个问题,那么你需要锁定。但是addAll仍然是线程安全的,所以队列不会被破坏。

但通常,队列用于在许多读者/写入器之间提供一种通信方式,并且可能不需要严格保留插入顺序。

现在,主要问题是,如果队列已满,add方法会抛出异常,因此addAll操作可能会在中间崩溃,并且您不知道哪些元素被添加了哪些元素没有添加。

如果您的用例允许等待空间来插入元素,那么您应该在循环中使用put。

for (E e: someCollection) queue.put(e);

这将阻塞,直到有空间添加其他元素。

手动锁定很棘手,因为在访问队列时,您必须始终记住添加锁定,这很容易出错。因此,如果您真的需要原子性,那么编写一个包装类来实现BlockingQUeue接口,但在调用底层操作之前使用锁定。

 类似资料:
  • 本文向大家介绍如何理解Java中的StringBuffer是线程安全的而StringBuilder是非线程安全的?,包括了如何理解Java中的StringBuffer是线程安全的而StringBuilder是非线程安全的?的使用技巧和注意事项,需要的朋友参考一下 StringBuffer(线程安全) StringBuffer是线程安全的,这意味着它们具有同步方法来控制访问,因此一次只有一个线程可以

  • 问题内容: 在我的Java项目中,我需要以多种方式使用TreeMap。我发现ConcurrentSkipListMap是我需要的,但是某些方法不是线程安全的。其中之一- containsKey(Object key)。以多种方式使用此方法的典型解决方案是什么?在我的程序中,我需要放置不会替换旧密钥的密钥,如果不可能的话,我将放置另一个密钥而不会获得唯一密钥。因为我不会丢失信息,所以应该使用哪种构造

  • 问题内容: 我看过OpenJDK的OpenJDK源代码,似乎所有写操作都受同一锁保护,而读操作则根本不受保护。据我了解,在JMM下,对变量的所有访问(读和写)都应受锁保护,否则可能会发生重新排序的效果。 例如,method包含以下几行(处于锁定状态): 另一方面,该方法仅起作用。 在我对JMM的理解中,这意味着如果将语句1-4重新排序为1-2(new)-4-2(copyOf)-3 ,则可能会在不一

  • 在我的应用程序中,我使用多个线程来处理客户端连接。 我在调试时发现了一个非常奇怪的行为——我有一个SelectionKey,通过调用(使用调试器)它的interestTops()方法,返回值是1(READ),但当我将数据发送到与该键对应的套接字时,选择器不会被唤醒。。 如果使用调试器,我将特定选择键更改为1(即使是1),选择器会突然对该更改做出反应。 在给定的时间内,我只有一个线程处理一个连接,但

  • 看起来它工作得很好,但我想知道我是否必须确保新代码需要线程安全?这里有什么意见吗?抱歉用了假名字,提前致谢。

  • IBM的支持指出,Class.GetAnnotation的实现不是线程安全的。 与其他JVM实现(例如,OpenJDK)相比,我们可以看到它们以线程安全的方式实现类方法。IBM JVM是一个闭源JVM,他们确实会发布一些源代码和他们的JVM一起,但无论什么时候他们的类实现是否线程安全,都不足以做出明确的判断。 类文档没有清楚地说明它的方法何时是线程安全的。那么,将类方法(特别是getAnnotat