|
JavaFAQ Home » General Java

Multithreading and concurrency are nothing new, but one of the innovations of
the Java language design was that it was the first mainstream programming
language to incorporate a cross-platform threading model and formal memory model
directly into the language specification. The core class libraries include a Thread
class for creating, starting, and manipulating threads, and the language
includes constructs for communicating concurrency constraints across threads -- synchronized
and volatile. While this simplifies the development of
platform-independent concurrent classes, it by no means makes writing concurrent
classes trivial -- just easier.
Declaring a block of code to be synchronized has two important consequences,
generally referred to as atomicity
and visibility.
Atomicity means that only one thread at a time can execute code protected by a
given monitor object (lock), allowing you to prevent multiple threads from
colliding with each other when updating shared state. Visibility is more subtle;
it deals with the vagaries of memory caching and compiler optimizations.
Ordinarily, threads are free to cache values for variables in such a way that
they are not necessarily immediately visible to other threads (whether it be in
registers, in processor-specific caches, or through instruction reordering or
other compiler optimizations), but if the developer has used synchronization, as
shown in the code below, the runtime will ensure that updates to variables made
by one thread prior to exiting a synchronized block will become
immediately visible to another thread when it enters a synchronized
block protected by that same monitor (lock). A similar rule exists for volatile
variables. (See Resources
for more information on synchronization and the Java Memory Model.)
synchronized (lockObject) {
// update object state
}
|
So synchronization takes care of everything needed to reliably update
multiple shared variables without race conditions or corrupting data (provided
the synchronization boundaries are in the right place), and ensures that other
threads that properly synchronize will see the most up-to-date values of those
variables. By defining a clear, cross-platform memory model (which was modified
in JDK 5.0 to fix some errors in the initial definition), it becomes possible to
create "Write Once, Run Anywhere" concurrent classes by following this
simple rule:
Whenever you will be writing a variable that may next be read by another
thread, or reading a variable that may have last been written by another
thread, you must synchronize.
Even better, in recent JVMs, the performance cost of uncontended
synchronization (when no thread attempts to acquire a lock when another thread
already holds it) is quite modest. (This was not always true; synchronization in
early JVMs had not yet been optimized, giving rise to the then-true, but now
mythical belief that synchronization, whether contended or not, has a big
performance cost.)
Improving on synchronized
So synchronization sounds pretty good, right? Then why did the JSR 166 group
spend so much time developing the java.util.concurrent.lock
framework? The answer is simple -- synchronization is good, but not perfect. It
has some functional limitations -- it is not possible to interrupt a thread that
is waiting to acquire a lock, nor is it possible to poll for a lock or attempt
to acquire a lock without being willing to wait forever for it. Synchronization
also requires that locks be released in the same stack frame in which they were
acquired, which most of the time is the right thing (and interacts nicely with
exception handling), but a small number of cases exist where
non-block-structured locking can be a big win.
The Lock framework in java.util.concurrent.lock is
an abstraction for locking, allowing for lock implementations that are
implemented as Java classes rather than as a language feature. It makes room for
multiple implementations of Lock, which may have different
scheduling algorithms, performance characteristics, or locking semantics. The ReentrantLock
class, which implements Lock, has the same concurrency and memory
semantics as synchronized, but also adds features like lock
polling, timed lock waits, and interruptible lock waits. Additionally, it offers
far better performance under heavy contention. (In other words, when many
threads are attempting to access a shared resource, the JVM will spend less time
scheduling threads and more time executing them.)
What do we mean by a reentrant
lock? Simply that there is an acquisition count associated with the lock, and if
a thread that holds the lock acquires it again, the acquisition count is
incremented and the lock then needs to be released twice to truly release the
lock. This parallels the semantics of synchronized; if a thread
enters a synchronized block protected by a monitor that the thread already owns,
the thread will be allowed to proceed, and the lock will not be released when
the thread exits the second (or subsequent) synchronized block, but
only will be released when it exits the first synchronized block it
entered protected by that monitor.
In looking at the code example in Listing 1, one immediate difference between
Lock and synchronization jumps out -- the lock must be released in
a finally block. Otherwise, if the protected code threw an exception, the lock
might never be released! This distinction may sound trivial, but, in fact, it is
extremely important. Forgetting to release the lock in a finally block can
create a time bomb in your program whose source you will have a hard time
tracking down when it finally blows up on you. With synchronization, the JVM
ensures that locks are automatically released.
Full
text here
Printer Friendly Page
Send to a Friend
..
Search here again if you need more info!
|