Explicit locks in Java 5
Before Java 5, the language offered two key synchronization facilities:
the synchronized
keyword allows exclusive locking at the level of a block or method, and
in tandem with this locking mechanism, the
wait/notify idiom effectively
allows threads to wait, interruptably, for a 'signal' from another thread.
The standard form of locking provided by the synchronized keyword
has limitations:
- Once a thread decides to enter a synchronized section, it commits
to potentially waiting forever for the lock to become available.
In particular, this means that cases where we need to hold more than
one lock to perform our
operation are difficult. Because synchronized is an all-or-nothing
operation, we can't, for example, define a "back-off policy" if one lock is quickly available
but the other isn't.
- Only one thread can hold the lock at once: there's no facility, for example,
to allow multiple threads holding a lock simultaneously for read-only access;
- Locking with synchronized happens purely at the block level:
one block cannot acquire a lock which is then released in another block
(this functionality is sometimes useful for traversing structures consisting
of linked nodes, where we want to lock only the node(s) we're currently accessing,
and effectively 'pass on the lock' as we traverse the structure).
- The fact that a block is synchronized is essentially determined at
compile time. At runtime, there's no good way to say "only synchronized
if X" (e.g. "only synchronize on the database connection if it's the shared
one, not a thread-local one")1.
- From our Java program, we can't get any "metadata" about the lock: that is
performance information such as how many threads are currently waiting on the
lock, on average how long it takes to acquire the lock etc. (Depending on your VM,
there may be roundabout methods to get such information from outside
the program, with varying degrees of difficulty.)
Of course, it does have the advantage of simple syntax and built-in support
from the JVM (people who have stuck to good-old synchronized
have seen their tenacity rewarded with gradual performance improvements over
the course of several JVM updates). But there is another approach: we could
perform synchronization via explicit locks.
What is an explicit lock?
When you use the synchronize word, what happens— and what
is to some extent hidden from view of the programmer— is that the thread
must acquire a lock on the object being synchronized on before entering
the synchronized block, and then release that lock when it exits the block. So
the following:
synchronized (someObject) {
// do some stuff
}
is in effect a syntactic shorthand for something like this:
someObject.lock.acquire();
try {
// do some stuff
} finally {
someObject.lock.release();
}
Each lock can be "owned" by up to one thread at any time. When a thread tries to
acquire ownership of the lock, it must wait if some other thread already has ownership.
Once the owning thread releases the lock,
if there are any threads sitting waiting to acquire it, one of them will then be able to proceed.
Now in fact, the lock in question— often called a monitor— is a hidden
construct inside the JVM, and not something we have access to from our Java program. So if we
use synchronized, we must accept the behaviour mentioned above. But there's nothing
particularly magical about the built-in synchronization locks (other than they're
"built in" and have been tuned for good performance under "typical" conditions):
so long as we can create some Java object that has the acquire/release
functionality mentioned, and make it perform2 adequately, then we could use that instead, and actually code our
synchronized "explicitly", much like the second code fragment above. This becomes interesting
when we consider variants of our lock, such as the following, where we try for two seconds
to acquire the lock, but then "back off" after that time, thus preventing deadlock:
if (!lock.acquireWithTimeout(2000L)) {
throw new RuntimeException("Couldn't get lock");
}
try {
// do some stuff
} finally {
lock.release();
}
Explicit locks in Java 5
With judicious use of wait/notify, it is
actually possible to construct a lock object pre-Java 5. But
Java 5 and the concurrency packages provide two key characteristics that make
explicit locks more viable:
- some explicit lock classes
are provided "out of the box";
- via AtomicInteger
and related classes, we can "roll or own" locks more efficiently than would have been
possibly with wait/notify.
1. There is a bad way: you could set which object gets synchronized on at runtime,
and to mean "don't synchronize", you can synchronize on new Object().
2. Lock performane is something we look at in more detail later, but encapsulates ideas
you might expect: we want to minimise the typical and worst-case time needed to acquire the
lock; we want to minimise "dead time" between a lock becoming available and the next
waiter acquiring it; and irrespective of "raw" times,
we may want the lock to scale in a particular way as the number of contending
threads increases.
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.