简体   繁体   中英

Thread safety of a queue backed by ConcurrentHashMap

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class DedupingQueue<E> implements QueueWrapper<E> {
    private static final Logger LOGGER = LoggerFactory.getLogger(DedupingQueue.class);

private final Map<E, Future<E>> itemsBeingWorkedOn = new ConcurrentHashMap<>();
private final AsyncWorker<E> asyncWorker; //contains async method backed by a thread pool

public DedupingQueue(AsyncWorker<E> asyncWorker) {
    this.asyncWorker = asyncWorker;
}

@Override
public Future<E> submit(E e) {
    if (!itemsBeingWorkedOn.containsKey(e)) {
        itemsBeingWorkedOn.put(e, asyncWorker.executeWorkAsync(e, this));
    } else {
        LOGGER.debug("Rejected [{}] as it's already being worked on", e);
    }
    return itemsBeingWorkedOn.get(e);
}

@Override
public void complete(E e) {
    LOGGER.debug("Completed [{}]", e);
    itemsBeingWorkedOn.remove(e);
}

@Override
public void rejectAndRetry(E e) {
    itemsBeingWorkedOn.putIfAbsent(e, asyncWorker.executeWorkAsync(e, this));
}

}

I am having some difficulty reasoning the thread safeness of the above code.

I reckon complete and rejectAndretry are completely thread-safe as the map is thread safe. But what about submit , given that AsyncWorker per se is not thread safe? Also, how can I make it thread safe in the most efficient way, without using synchronized (using built in guarantees of ConcurrentHashMap) ?

submit() isn't thread safe because your check-then-act sequence isn't atomic. State for a particular key for one thread can be changed by other thread right after check and before invocation. Use an atomic method from ConcurrentHashMap (most likely computeIfAbsent() ) to fix this issue.

if (!itemsBeingWorkedOn.containsKey(e)) {
    itemsBeingWorkedOn.put(e, asyncWorker.executeWorkAsync(e, this));
}

If AsyncWorker isn't thread safe what whole class isn't thread safe as well. But it's hard to reason about it without AsyncWorker source code.

There's the compute() method, which function takes the key, the current value or null if there's no current mapping, and returns the value to keep in the map, or remove it if null is returned.

@Override
public Future<E> submit(E e) {
    return itemsBeingWorkedOn.compute(e, (k, v) -> {
        if (v == null) {
            return asyncWorker.executeWorkAsync(k, this);
        } else {
            LOGGER.debug("Rejected [{}] as it's already being worked on", k);
            return v;
        }
    });
}

Note that your queue is not ordered. If you ever need FIFO or LIFO order, you should use a more suitable data structure, such as LinkedHashMap with synchronized statements.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM