Search code examples
multithreadingconcurrencythread-safetyjava.util.concurrentconcurrenthashmap

ConcurrentHashMap remove issue


I have a class like this

import java.util.concurrent.*;
public class TestClass<V> {
    private final ConcurrentMap<String, Future<V>> requests;
    private final ExecutorService executorService;

    public TestClass(final ExecutorService executorService) {
        this.executorService = executorService;
        this.requests = new ConcurrentHashMap<>();
    }

    public V submitRequest(String cacheKey, Callable<V> request) throws Exception {
        final Future<V> task = getOrCreateTask(cacheKey, request);
        final V results;
        try {
            results = task.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new IllegalStateException(String.format("Exception while executing request for key '%s'", cacheKey),
                    e);
        } finally {
            //Nullpointer here
            requests.remove(cacheKey);
        }

        return results;
    }


    private synchronized Future<V> getOrCreateTask(String key, Callable<V> request) {
        if (requests.containsKey(key)) {

            return requests.get(key);
        } else {

            final Future<V> newTask = executorService.submit(request);
            requests.put(key, newTask);
            return newTask;
        }


       }
    }

but sometimes under heavy load server throws nullpointer on requests.remove(cacheKey). I have read final when not escaped by this in the constructor is write guaranteed. i.e. other threads can see what is going on with my requests map.

Not sure how do i fix efficiently? Does not like that idea of adding synchronised on the whole parent level method


Solution

  • I'm not actually sure the NPE is where you're identifying it is unless cacheKey is null, which you could check for. The concurrentmap is set correctly so the requests field should never be null. Nevertheless, this code is not correctly synchronized. You are attempting to perform two operations in getOrCreateTask() that while under the synchronized keyword are not correctly synchronized with the map because the map is interacted with in submitRequest when you remove the values.

    What is likely happening is that between the check ConcurrentMap#containsKey and ConcurrentMap#get that another thread has removed the value from the cache (ConcurrentMap#remove).

    • Thread A: Check Contains "foobar" => true
    • Thread B: Remove "foobar"
    • Thread A: Call get("foobar") => null
    • Thread A: Calls Future#get on a null pointer, which then throws a NPE.

    Since you control the concurrentmap you can know you'll never have null values. In that case you should instead just call the #get method and check if the returned value is null. This will prevent another thread from removing the value between a contains/get pair since you'll be only accessing the map through one atomic operation.