Search code examples
javamultithreadingcachingdata-structuresthread-safety

ConcurrentHashMap: remove on condition


I have a ConcurrentHashMap which used as an in-memory storage (or a cache, you might say)

What I'd like to achieve is: Concurrently check if an item "is ready", and if so, remove it from the map (+ return it to the caller). There's no direct method that enables me to do that.

The only solution I came up with is having an ItemContainer which will contain both the item and meta-data (isReady field). On every access, I'll have to apply merge or compute operations. Essentially replacing the container of the object on every access/check.

Questions:

  1. Is my solution seem reasonable?
  2. Are there any good libraries that achieves something similar?

I added a "boilerplate" code as requested:

public class Main {

    public static void main(String[] args) {
        Storage storage = new Storage();
        storage.put("1", new Record("s", 100));
        storage.put("2", new Record("s", 4));
        storage.removeIf("1", Main::isReady);
    }

    public static boolean isReady(Record record) {
        return record.i > 42;
    }

    public static class Record {

        public Record(String s, Integer i) {
            this.s = s;
            this.i = i;
        }

        String s;
        Integer i;
    }

    public static class Storage {
        ConcurrentHashMap<String, Record> storage = new ConcurrentHashMap<>();

        public void put(String key, Record record) {
            storage.put(key, record);
        }

        public Record removeIf(String key, Function<Record, Boolean> condition) {
            return null; // TODO: implement
        }
    }
}

Other solutions (with tradeoffs):

  1. Always remove() on check and then merge() it back to the map.
  2. Use a cache with some reasonable policy of items evacuation (i.e. LRU) and check only evacuated items.

Based on @ernest_k solution:

public Record removeIf(String key, Predicate<Record> condition) {
    AtomicReference<Record> existing = new AtomicReference<>();

    this.storage.computeIfPresent(key, (k, v) -> {
        boolean conditionSatisfied = condition.test(v);

        if (conditionSatisfied) {
            existing.set(v);
            return null;
        } else {
            existing.set(null);
            return v;
        }
    });

    return existing.get();
}

Solution

  • ConcurrentHashMap already gives you the guarantee of atomicity with computeIfPresent.

    If the value for the specified key is present, attempts to compute a new mapping given the key and its current mapped value. The entire method invocation is performed atomically. Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this map.

    So you can just use that:

    public Record removeIf(String key, Predicate<Record> condition) {
    
        AtomicReference<Record> existing = new AtomicReference<>();
    
        this.storage.computeIfPresent(key, (k, v) -> {
            existing.set(v);
            return condition.test(v) ? null : v;
        });
    
        return existing.get();
    }
    

    Note that I used Predicate<Record> as it should be preferred to Function<Record, Boolean>.

    The reason for storing the current value in an AtomicReference here is to make sure that the returned value is the same one that the predicate was testedd against (otherwise there might be a race condition).