Using a ConcurrentMap
will usually generate better performance than a synchronized
block.
Java 5-7:
ConcurrentMap<String, ReadWriteLock> lockMap = new ConcurrentHashMap<>();
ReadWriteLock getLock(String key) {
ReadWriteLock lock = lockMap.get(key);
if (lock == null) {
lock = new ReentrantReadWriteLock();
ReadWriteLock other = lockMap.putIfAbsent(key, lock);
if (other != null) {
// another thread's putIfAbsent won
lock = other;
}
}
return lock;
}
Java 8+:
ConcurrentMap<String, ReadWriteLock> lockMap = new ConcurrentHashMap<>();
ReadWriteLock getLock(String key) {
return lockMap.computeIfAbsent(key, ReentrantReadWriteLock::new);
}
For one, an implementation like ConcurrentHashMap
is documented to not use locks on read operations. So in this case, where it appears that you intend to get the lock for a single key many more times than you intend to create new locks, that will reduce thread contention. If you used synchronized
, even if the lock was already created, you're forcing each and every thread to go single file through the critical section.
Also, implementations can do more advanced forms of locking, or even shard locks so that two writers don't necessarily block each other (if writing to different partitions of the underlying data structure). Again, synchronized
uses a single monitor object and can't benefit from knowing the details of the underlying data structure.
The Java 8 version becomes a one-liner thanks to lambdas and function references. The ::new
syntax refers to the public, no-arg constructor of the adjoining ReentrantReadWriteLock
class. The computeIfAbsent
method will only invoke that constructor if necessary and basically does all of the boilerplate work in the Java 7 version above for you. This is particularly useful if the cost of creating the new object is expensive or has unfortunate side effects. Note that the Java 7 version has to create a new lock instance under certain circumstances and that new object might not ever be used/returned.