4

Quoting the documentation:

"BlockingQueue implementations are thread-safe. All queuing methods achieve their effects atomically using internal locks or other forms of concurrency control. However, the bulk Collection operations addAll, containsAll, retainAll and removeAll are not necessarily performed atomically unless specified otherwise in an implementation. So it is possible, for example, for addAll(c) to fail (throwing an exception) after adding only some of the elements in c."

Since there isn't written anything in particular in the description of the LinkedBlockingQueue.addAll() operation, I have to assume that's not thread safe.

Do you agree with me that the only solution in order to guarantee that all the elements added through an addAll() are contiguous (i.e. added together) is to use a Lock everytime that the queue is modified (with an add or take operation)? For example:

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: Wow nobody saw a big bug in the previous example: since the take() is a blocking operation, and since the lock is needed in order to add element to the queue, as soon as the queue will be empty the program will enter in a deadlock state: a writer cannot write since the lock is possessed by the take(), and at the same time the take() will be in a blocked state until something is written in the queue (which it can't happen for the previous reason). Any idea? I think that the most obvious one is remove the lock around the take() but then maybe the desired atomicity of addAll() is not guaranteed.

justHelloWorld
  • 6,478
  • 8
  • 58
  • 138

2 Answers2

3

addAll is still thread safe it just does not provide atomicity. So it depends what is your use case / expectation.

If you use addAll without the explicit locking, then if other Thread tries to write to the queue (add new element(s)), the order of added elements is not guaranteed and they may get mixed. If it is an issue than yes you need locking. But addAll is still thread safe, there will be no corruption of the queue.

But typically, the queue is used to provide a way of communication between many Readers/Writers and strict preservation of insertion order may not be needed.

Now, the main problem is, that add method throws an exception if the queue is full, so that addAll operation can collapse in the middle and you don't know which elements were added which weren't.

If your use case allows waiting for the space to insert the elements, than you should rather use put in a loop.

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

This will block till there is space to add another elements.

Manually locking is tricky, because you always have to remember to add locking when you access the queue, which is error prone. So if you really need the atomicity, write a wrapper class that implements the BlockingQUeue interface but uses locking before calling the underlying operations.

Zielu
  • 8,312
  • 4
  • 28
  • 41
  • Unfortunately, the element inserted through the addAll() cannot be mixed with other elements added through adds operations: if the addAll() operation is temporally called before a normal add(), then the element appended to the queue must be after any eleement added through the first operation. – justHelloWorld Mar 10 '15 at 17:49
  • `LinkedBlockingQueue` is not size-bounded. Regarding using a wrapper class, I totally agree. – fps Mar 10 '15 at 18:12
  • Look at my IMPORTANT UPDATE bug ;) – justHelloWorld Mar 11 '15 at 18:06
  • 1
    You dont need lock for take operations. It is thread safe and it is preserves the order. Only the writes have to be protected. – Zielu Mar 11 '15 at 18:11
  • @Zielu I agree with you :) – justHelloWorld Mar 11 '15 at 18:49
0

I believe you're confusing thread-safe with atomic. As I understand it, bulk operations are thread-safe, though not atomic.

I don't think you need to use an external ReentrantLock in order to make your BlockingQueue be thread-safe. Actually, addAll() is implemented by iterating over the given Collection and calling add() on the queue for each element of the collection. As add() is thread-safe, then you wouldn't need to synchronize anything.

When the javadocs say this:

So it is possible, for example, for addAll(c) to fail (throwing an exception) after adding only some of the elements in c.

it means that only some elements of the given collection c might have been added when addAll(c) returns. However it doesn't mean that you'd need to lock on the queue to invoke take() or any other operation.

EDIT:

As per your use case, you could use a lock as you suggested, though I would make it internal to the BlockingQueue implementation, so that caller classes wouldn't need to adhere to the lock/call_some_method_from_the_queue/unlock pattern. I'd be more careful when using the lock, i.e. use it in a try/finally block:

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
}
fps
  • 33,623
  • 8
  • 55
  • 110
  • 1
    My main issue is that I have to guarantee that all the element added through the addAll() operation are contiguous, and so without the lock there is the possibility that they are mixed with other element inserted during other concurrent write operation. Please, tell me if I'm wrong ;) – justHelloWorld Mar 10 '15 at 17:46
  • You're not wrong. If you call `add(oneElem)` from one thread and `addAll(manyElems)` from another, `oneElem` might end up being inserted "in the middle" of `someElems`. Do you need to rely on the insertion order of the queue? – fps Mar 10 '15 at 17:54
  • Absolutely yes: my application is for a Distributed System project, so in order to guarantee serializability, if an operation (or a set of write operation) is committed (added to the list) before a another operation, then this order has to be maintained. – justHelloWorld Mar 10 '15 at 18:05
  • @justHelloWorld OK, I'll edit my answer with a suggestion, meybe it's useful to you. – fps Mar 11 '15 at 14:27
  • Look at my IMPORTANT UPDATE bug ;) – justHelloWorld Mar 11 '15 at 18:06
  • @justHelloWorld To be honest, I didn't see the bug you mention, but I was about to suggest you didn't lock on any operation. Please wait for my edit, I'm quite busy right now. – fps Mar 11 '15 at 18:42
  • @justHelloWorld Besides, you have a typo here: `queue.addLock(someCollection);`. It should be `queue.addAll(someCollection);`. – fps Mar 11 '15 at 18:45