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.
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
'sIterator
does not support theremove()
operation
- Not all, however. I believe
- 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:
Thread-A
checks size condition, result isfalse
Thread-A
moves to add newOperation
- Before
Thread-A
can add theOperation
,Thread-B
checks size condition which results infalse
as well Thread-B
goes to add newOperation
Thread-A
does add newOperation
- Oh, no! The
Operation
added byThread-A
causes the size threshold to be reached
- Oh, no! The
Thread-B
, already past theif
statement, adds itsOperation
making the deque have one too manyOperation
s
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();
}