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

时间:2015-03-10 16:04:02

标签: java thread-safety locking blockingqueue

引用文档:

  

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

由于在LinkedBlockingQueue.addAll()操作的描述中没有特别编写任何内容,我必须假设它不是线程安全的。

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

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();

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

2 个答案:

答案 0 :(得分:2)

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

如果在没有显式锁定的情况下使用addAll,那么如果其他Thread尝试写入队列(添加新元素),则无法保证添加元素的顺序,并且它们可能会混合。如果它是一个问题而不是肯定你需要锁定。但是addAll仍然是线程安全的,队列没有损坏。

但通常,队列用于提供许多读者/作者之间的通信方式,并且可能不需要严格保存插入顺序。

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

如果你的用例允许等待空格插入元素,那么你应该使用put in循环。

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

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

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

答案 1 :(得分:0)

我相信您会将线程安全 atomic 混淆。据我了解,批量操作是线程安全的,但不是原子的。

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

当javadocs这样说:

  

因此,例如,在仅添加c中的一些元素之后,addAll(c)可能会失败(抛出异常)。

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

修改

根据您的使用情况,您可以按照建议使用锁,但我会将其设置为BlockingQueue实现的内部,这样调用者类就不需要遵守lock / call_some_method_from_the_queue /解锁模式。使用锁时我会更加小心,即在try/finally块中使用它:

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
}