visit
In this post, I want to revisit the old ways of doing threading in Java. I’m used to synchronized
, wait
, notify
, etc. But it has been a long time since they were the superior approach for threading in Java.
synchronized(LOCK) {
// safe code
}
LOCK.lock();
try {
// safe code
} finally {
LOCK.unlock();
}
The first disadvantage of ReentrantLock
is the verbosity. We need the try block since if an exception occurs within the block, the lock will remain. Synchronized handles that seamlessly for us.
There’s a trick some people pull of wrapping the lock with AutoClosable
which looks roughly like this:
public class ClosableLock implements AutoCloseable {
private final ReentrantLock lock;
public ClosableLock() {
this.lock = new ReentrantLock();
}
public ClosableLock(boolean fair) {
this.lock = new ReentrantLock(fair);
}
@Override
public void close() throws Exception {
lock.unlock();
}
public ClosableLock lock() {
lock.lock();
return this;
}
public ClosableLock lockInterruptibly() throws InterruptedException {
lock.lock();
return this;
}
public void unlock() {
lock.unlock();
}
}
Notice I don’t implement the Lock interface which would have been ideal. That’s because the lock method returns the auto-closable implementation instead of void
.
try(LOCK.lock()) {
// safe code
}
The biggest reason for using ReentrantLock
is Loom support. The other advantages are nice, but none of them is a “killer feature”.
lockInterruptibly()
lets us interrupt a thread while it’s waiting for a lock. This is an interesting feature, but again, hard to find a situation where it would realistically make a difference.
If you write code that must be very responsive for interrupting, you would need to use the lockInterruptibly()
API to gain that capability. But how long do you spend within the lock()
method on average?
A much better approach is the ReadWriteReentrantLock
. Most resources follow the principle of frequent reads, and few write operations. Since reading a variable is thread-safe, there’s no need for a lock unless we’re in the process of writing to the variable.
E.g., in the following code, we expose the list of names as read-only, but then when we need to add a name we use the write lock. This can outperform synchronized
lists easily:
private final ReadWriteLock LOCK = new ReentrantReadWriteLock();
private Collection<String> listOfNames = new ArrayList<>();
public void addName(String name) {
LOCK.writeLock().lock();
try {
listOfNames.add(name);
} finally {
LOCK.writeLock().unlock();
}
}
public boolean isInList(String name) {
LOCK.readLock().lock();
try {
return listOfNames.contains(name);
} finally {
LOCK.readLock().unlock();
}
}
The first thing we need to understand about StampedLock
is that it isn’t reentrant. Say we have this block:
synchronized void methodA() {
// …
methodB();
// …
}
synchronized void methodB() {
// …
}
This will work. Since synchronized is reentrant. We already hold the lock, so going into methodB()
from methodA()
won’t block. This works with ReentrantLock too assuming we use the same lock or the same synchronized object.
StampedLock
returns a stamp that we use to release the lock. Because of that, it has some limits. But it’s still very fast and powerful. It too includes a read-and-write stamp we can use to guard a shared resource.
But unlike the ReadWriteReentrantLock,
it lets us upgrade the lock. Why would we need to do that?
Look at the addName()
method from before… What if I invoke it twice with “Shai”?
Yes, I could use a Set… But for the point of this exercise, let's say that we need a list… I could write that logic with the ReadWriteReentrantLock
:
public void addName(String name) {
LOCK.writeLock().lock();
try {
if(!listOfNames.contains(name)) {
listOfNames.add(name);
}
} finally {
LOCK.writeLock().unlock();
}
}
This sucks. I “paid” for a write lock only to check contains()
in some cases (assuming there are many duplicates). We can call isInList(name)
before obtaining the write lock. Then we would:
With a StampedLock
, we can update the read lock to a write lock and do the change on the spot if necessary as such:
public void addName(String name) {
long stamp = LOCK.readLock();
try {
if(!listOfNames.contains(name)) {
long writeLock = LOCK.tryConvertToWriteLock(stamp);
if(writeLock == 0) {
throw new IllegalStateException();
}
listOfNames.add(name);
}
} finally {
LOCK.unlock(stamp);
}
}
It is a powerful optimization for these cases.
You might think, why can’t synchronized
collections use ReadWriteReentrantLock
or even StampedLock
?