Check size and then perform operation - is it safe for ConcurrentLinkedDeque?

Izbassar Tolegen :

I need to replace the first value in Deque with the new value, only if the size will exceed the limit. I wrote this code to solve it:

final class Some {
    final int buffer;
    final Deque<Operation> operations = new ConcurrentLinkedDeque<>();
    // constructors ommited;

    @Override
    public void register(final Operation operation) {
        if (this.operations.size() == this.buffer) {
            // remove the oldest operation
            this.operations.removeFirst();
        }
        // add new operation to the tail
        this.operations.addLast(operation);
    }

    @Override
    public void apply() {
        // take the fresh operation from tail and perform it
        this.operations.removeLast().perform();
    }
}

As you see, I have two methods, that modifies the Deque. I have doubts, that this code will work correctly in the multithreaded environment. The question is: is it safe to check the size() and then performing operations, that modifies the ConcurrentLinkedDeque afterward? I want to have as least locks as possible. So if this code won't work, then I had to introduce locking and then there is no point in the usage of ConcurrentLinkedDeque().

final class Some {
    final int buffer;
    final Deque<Operation> operations = new LinkedList<>();
    final Lock lock = new ReentrantLock();
    // constructors ommited;

    @Override
    public void register(final Operation operation) {
        this.lock.lock();
        try {
            if (this.operations.size() == this.buffer) {
                // remove the oldest operation
                this.operations.removeFirst();
            }
            // add new operation to the tail
            this.operations.addLast(operation);
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void apply() {
        this.lock.lock();
        try {
            // take the fresh operation from tail and perform it
            this.operations.removeLast().perform();
        } finally {
            this.lock.unlock();
        }
    }
}

This is the alternative with the Lock. Is that the only way to achieve what I want? I am especially interested in trying to use the concurrent collections.

Slaw :

Concurrent collections are thread-safe when it comes to internal state. In other words, they

  • Allow multiple threads to read/write concurrently without having to worry that the internal state will become corrupted
  • Allow iteration and removal while other threads are modifying the collection
    • Not all, however. I believe CopyOnWriteArrayList's Iterator does not support the remove() operation
  • Guarantees things such as happens-before
    • Meaning a write by one thread will happen-before a read by a subsequent thread

However, they are not thread-safe across external method calls. When you call one method it will acquire whatever locks are necessary but those locks are released by the time the method returns. If you're not careful this can lead to a check-then-act race condition. Looking at your code

if (this.operations.size() == this.buffer) {
    this.operations.removeFirst();
}
this.operations.addLast(operation);

the following can happen:

  1. Thread-A checks size condition, result is false
  2. Thread-A moves to add new Operation
  3. Before Thread-A can add the Operation, Thread-B checks size condition which results in false as well
  4. Thread-B goes to add new Operation
  5. Thread-A does add new Operation
    • Oh, no! The Operation added by Thread-A causes the size threshold to be reached
  6. Thread-B, already past the if statement, adds its Operation making the deque have one too many Operations

This is why a check-then-act requires external synchronization, which you do in your second example using a Lock. Note you could also use a synchronized block on the Deque.

Unrelated to your question: You call Operation.perform() in your second example while still holding the Lock. This means no other thread can attempt to add another Operation to the Deque while perform() executes. If this isn't desired you can change the code like so:

Operation op;

lock.lock();
try {
    op = deque.pollLast(); // poll won't throw exception if there is no element
} finally {
    lock.unlock();
}

if (op != null) {
    op.perform();
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=89766&siteId=1