Your workaround is actually very good. There are other objects on top of which you can build a somewhat similar solution (for example, using computeIfPresent() values โโand tombstones), but they have their own warnings, and I used them in several different use cases.
As for using a type that does not implement equals() for map values, you can use your own wrapper on top of the corresponding type. This is the easiest way to introduce custom semantics for equality of objects in atom replace / delete operations provided by ConcurrentMap .
Update
Here is a sketch that shows how you can build on top of the ConcurrentMap.remove(Object key, Object value) API:
- Define the shell type on top of the mutable type that you use for the values, also defining your own
equals() method built on top of the current mutable value. - In
BiConsumer (the lambda that you switch to forEach ), create a deep copy of the value (like your new shell type) and follow your logic to determine if that value should be deleted on the copy. - If the value needs to be removed, call
remove(myKey, myValueCopy) .- If it was necessary to
remove(myKey, myValueCopy) while calculating any number of simultaneous changes, remove(myKey, myValueCopy) will return false (banning ABA problems, which are a separate topic).
Here are some examples to illustrate this:
import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; public class Playground { private static class AtomicIntegerWrapper { private final AtomicInteger value; AtomicIntegerWrapper(int value) { this.value = new AtomicInteger(value); } public void set(int value) { this.value.set(value); } public int get() { return this.value.get(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof AtomicIntegerWrapper)) { return false; } AtomicIntegerWrapper other = (AtomicIntegerWrapper) obj; if (other.value.get() == this.value.get()) { return true; } return false; } public static AtomicIntegerWrapper deepCopy(AtomicIntegerWrapper wrapper) { int wrapped = wrapper.get(); return new AtomicIntegerWrapper(wrapped); } } private static final ConcurrentMap<Integer, AtomicIntegerWrapper> MAP = new ConcurrentHashMap<>(); private static final int NUM_THREADS = 3; public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 10; ++i) { MAP.put(i, new AtomicIntegerWrapper(1)); } Thread.sleep(1); for (int i = 0; i < NUM_THREADS; ++i) { new Thread(() -> { Random rnd = new Random(); while (!MAP.isEmpty()) { MAP.forEach((key, value) -> { AtomicIntegerWrapper elem = MAP.get(key); if (elem == null) { System.out.println("Oops..."); } else if (elem.get() == 1986) { elem.set(1); } else if ((rnd.nextInt() & 128) == 0) { elem.set(1986); } }); } }).start(); } Thread.sleep(1); new Thread(() -> { Random rnd = new Random(); while (!MAP.isEmpty()) { MAP.forEach((key, value) -> { AtomicIntegerWrapper elem = AtomicIntegerWrapper.deepCopy(MAP.get(key)); if (elem.get() == 1986) { try { Thread.sleep(10); } catch (Exception e) {} boolean replaced = MAP.remove(key, elem); if (!replaced) { System.out.println("Bailed out!"); } else { System.out.println("Replaced!"); } } }); } }).start(); } }
You will see printouts of "Bailed out!" mixed with "Replaced!". (the deletion was successful, since there were no parallel updates you care about), and the calculation will stop at some point.
- If you delete the custom
equals() method and continue to use the copy, you will see an endless stream of "Bailed out!" because the copy is never considered equal to the value on the map. - If you are not using a copy, you will not see "Bailed out!" printed, and you run into a problem that you explain - the values โโare deleted regardless of the simultaneous changes.
Dimitar Dimitrov
source share