Guava has something like this being released in 13.0; you can get it out of HEAD if you like.

Striped<Lock> more or less allocates a specific number of locks, and then assigns strings to locks based on their hash code. The API looks more or less like

Striped<Lock> locks = Striped.lock(stripes);
Lock l = locks.get(string);
l.lock();
try {
  // do stuff 
} finally {
  l.unlock();
}

More or less, the controllable number of stripes lets you trade concurrency against memory usage, because allocating a full lock for each string key can get expensive; essentially, you only get lock contention when you get hash collisions, which are (predictably) rare.

(Disclosure: I contribute to Guava.)


private static final Set<String> lockedKeys = new HashSet<>();

private void lock(String key) throws InterruptedException {
    synchronized (lockedKeys) {
        while (!lockedKeys.add(key)) {
            lockedKeys.wait();
        }
    }
}

private void unlock(String key) {
    synchronized (lockedKeys) {
        lockedKeys.remove(key);
        lockedKeys.notifyAll();
    }
}

public void doSynchronously(String key) throws InterruptedException {
    try {
        lock(key);

        //Do what you need with your key.
        //For different keys this part is executed in parallel.
        //For equal keys this part is executed synchronously.

    } finally {
        unlock(key);
    }
}

try-finally - is very important - you must guarantee to unlock waiting threads after your operation even if your operation threw exception.