Problems with the Java 1.4 synchronization model
Java's bread-and-butter means of synchronizing data– the synchronized
method– has some advantages. It's reasonably easy to understand and implement,
and generally allows you to write thread-safe code without getting too bogged down
in the details of what the JVM does "under the hood" to synchronize data.
Of course, this is also one of its potential downfalls– a failure to
understand exactly what synchronized does has led to programmers using
incorrect idioms
such as "double-checked locking" or the notion that "you only need to synchronize
on the write, not the read".
But even in a perfect world where every programmer fully understands the
synchronized keyword, as a method of synchronization it has some shortcomings:
- For some purposes, it's quite a heavy-handed means of synchronization.
For every object synchronized on, the JVM has to keep track of 'housekeeping'
information, such as which thread owns the lock and how many times it has acquired
it. And recall that every time any lock is acquired or released at the
beginning and end of a synchronized block, all cached variables
must be synchronized with main memory. There's no way to tell the JVM "you only
need to synchronize this one", nor is the JVM allowed to make such a decision.
- Relating to the previous point, there is no way to gives performance
hints to the JVM about how we expect the code in our synchronized block to behave.
We'll explain this point in more detail below.
- synchronized is an all-or-nothing thing. Once your thread
attemps to enter a synchronized block, it will hang forever until the
lock is available. In the real world, we often want to do things like "wait for
up to 2 seconds for the lock on the cache, else don't bother caching", for which
we need a more complex workaround. There's also the more serious risk of
deadlock: two threads each holding on to the lock that the other
thread needs to continue. In a complex server application that synchronizes on
many different objects in many points in the code, guaranteeing that deadlock does not
occur can be a very tricky issue.
- Once a thread does enter a synchronized block, we have no good way
of asking the JVM if our thread had to wait for the lock or not.
(We could time it, but calling System.currentTimeMillis() twice as we'd have
to can itself burn up to a millisecond of CPU time and in any case probably isn't
accurate enough.)
So from our normal running program, we can't profile lock contention and spot bottlenecks.
A limitation of pre-Java 5 as a platform is that the standard class library
doesn't provide implementations of some common synchronization idioms. For example, a
common use of synchronization in server applications is to manage a shared
resource pool (for example, of database connections). Rather than a simple lock,
what we really need in this case is a "permission" system that says "allow up to N threads to hold
a lock at any one time" (because there are N resources available in the pool). Idioms such
as this can of course be constructed pre Java 51 (else nobody would have been using Java to
run servers!) but not necessarily very efficiently. And without a standard library
implementation, different programmers have been forced
to re-invent the wheel, possibly in buggy ways.
So how does Java 5 improve this situation? On the next pages,
we look at Synchronization under the hood, and
how Java 5 improves on it.
Notes:
1. For some suggested pre-Java 5 implementations of idioms such as barriers, sempahores etc,
see Wellings, A. (2004), Concurrent and Real-Time Programming in Java.
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.