Fine-grained synchronization to serve a file I'm writing a proxy server that serves cached resources. If resource is not cached, it's retrieved and put into filesystem. Server is multi-threaded and I want to avoid any kind of global locking. If there are several requests for the same resource that is not cached at the same time, one request should retrieve resource, put it into filesystem and other requests should be blocked and then serve cached resource.

My first approach was to synchronize on interned string:

void serveFile(Path file) {
    if (!Files.exists(file)) {
        synchronized (file.toString().intern()) {
            if (!Files.exists(file)) {
                retrieveResource(file);
            }
        }
    }
    serveResource(file);
}


But there are many discussions about why this approach is not the best, so I rewrote is as follows:

private final ConcurrentMap&lt;Path, Object&gt; downloadLocks = new ConcurrentHashMap&lt;&gt;();

void serveFile(Path file) {
    if (!Files.exists(file)) {
        Object lock = downloadLocks.get(file);
        if (lock == null) {
            Object newLock = new Object();
            Object existingLock = downloadLocks.putIfAbsent(file, newLock);
            lock = existingLock == null ? newLock : existingLock;
        }
        try {
            synchronized (lock) {
                if (!Files.exists(file)) {
                    retrieveResource(file);
                }
            }
        } finally {
            downloadLocks.remove(file, lock);
        }
    }
    serveResource(file);
}


retrieveResource method retrieves resource into a temporary file and then performs atomical move into the final destination, so I assume that it's safe to use.

My question is if it's a correct multithreaded code and if there are better suited primitives for this fine-grained synchronization. I'm using Java 7.
Java's low level thread synchronization mechanism (synchronized blocks, volatile variables, wait(), notify(), etc) were proven to be tricky for the human single threaded mindset. Threading hazards like deadlock, thread starvation, and race conditions, which result from incorrect use of low level thread synchronization, are also hard to detect and debug.

To that end, Java 7 introduced several new features/classes in the java.util.concurrency package that allow developers to approach the thread synchronization domain from a high(er) level.  java.util.concurrent.locks.ReentrantLock is more flexible and gives more fine grain control over the scope of locking.  it also performs better.  The advice that I see around the web (including on Stack Overflow) is that ReentrantLock should replace synchronized block in all new code.