
18
the bit was not set, it sets it. This instruction is a crude sort of semaphore because when two threads
try to set the bit simultaneously, only one will actually do it. Both threads can then check to see if they
were the one that set the bit.
If the bit is set (i.e., there is contention), the JVM has to go out to the operating system to wait for the
bit to clear. Crossing the interprocess boundary into the operating system is expensive. In NT, it takes
on the order of 600 machine cycles just to enter the OS kernel, and this count doesn't include the
cycles spent doing whatever you entered the kernel to do. That's why passes 7 and 8 take so much
more time, because the JVM must interact with the operating system. Alexander Garthwaite from Sun
Labs brought up a few other interesting issues in a recent email to me:
Synchronized blocks are often different from synchronized methods in that the generated byte
code need not properly nest these. As a result, these are often slightly more expensive
(particularly in lock-release).
Some locking strategies use caches of monitors. So, the number and order in which objects are
locked can affect performance. More generally, the locking subsystem may use growable
structures for various purposes, and these will become more cumbersome to manage as the
number of locked objects increases.
Some locking strategies use a thin-lock/fat-lock strategy. In the thin lock, only simple
synchronization is supported and locking depth is often limited to a small number (often
somewhere between 16 and 64). Lock inflation occurs when this count is exceeded, when there
is contention on the lock, or when a wait or notify operation is performed. Lock deflation can
also add costs if it is supported at all.
For space efficiency in object headers, other information is often either stored in the same word
as locking state or it forces lock inflation. A common example is the object's hashcode() method.
This means that accessing this information in locked objects is often more expensive, and objects
with hash codes may be more expensive to lock than ones without.
One other thing that I'll add, if you can reduce the odds of contention, then the locking process is more
efficient. This reasoning implies that you should make the synchronization blocks as small as possible
so that a given lock will be unlocked most of the time.
Avoiding Synchronization
Fortunately, explicit synchronization is often avoidable. Methods that don't use any of the state
information (such as fields) of the class to which they belong don't need to be synchronized, for
example. (That is, they use only local variables and arguments—no class-level fields—and they don't
modify external objects by means of references that are passed in as arguments.) There are also
various class-based solutions, which I discuss in subsequent chapters (such as the synchronization
wrappers used by the Java collection classes).
You can sometimes eliminate synchronization simply by using the language properly, however. The
next few sections show you how.
Atomic Energy: Do Not Synchronize Atomic Operations
The essential concept vis-a-vis synchronization is atomicity. An "atomic" operation cannot be
interrupted by another thread, and naturally atomic operations do not need to be synchronized.
Java defines a few atomic operations. In particular, assignment to variables of any type except long
and double is atomic. To understand ramifications of this statement, consider the following (hideously
non-object-oriented) code:
class Unreliable
{ private long x;