By now it's quite obvious that the double-check-lock method of synchronizing for a singleton is broken. It has been comprehensively proved based on Java's memory model & the out-of-order writes. So, what's the alternative for this (while still loading lazily)?
The most famous solution seems to be from Bill Pugh where he employs an ingenious way of doing the same without the need for any synchronization. He does this by exploiting the Java's semantics guarantee that a class is loaded only when referenced for the first time.
Here's the implementation based on his solution (courtesy: Wikipedia).
public class Singleton {
// Private constructor prevents instantiation from other classes
private Singleton() { }
/**
* SingletonHolder is loaded on the first execution of Singleton.getInstance()
* or the first access to SingletonHolder.INSTANCE, not before.
*/
private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
More on JMM here (
http://www.ibm.com/developerworks/java/library/j-jtp08223/). In addition, an awesome explanation of how ConcurrentHashMap is implemented.
A JMM overview
Before we jump into the implementation of
put()
,
get()
, and
remove()
,
let's briefly review the JMM, which governs how actions on memory
(reads and writes) by one thread affect actions on memory by other
threads. Because of the performance benefits of using processor
registers and per-processor caches to speed up memory access, the Java
Language Specification (JLS) permits some memory operations not to be
immediately visible to all other threads. There are two language
mechanisms for guaranteeing consistency of memory operations across
threads --
synchronized
and
volatile
.
According to the JLS, "In the absence of explicit synchronization, an
implementation is free to update the main memory in an order that may be
surprising." This means that without synchronization, writes that
occur in one order in a given thread may appear to occur in a different
order to a different thread, and that updates to memory variables may
take an unspecified time to propagate from one thread to another.
While the most common reason for using synchronization is to guarantee
atomic access to critical sections of code, synchronization actually
provides three separate functions -- atomicity, visibility, and
ordering. Atomicity is straightforward enough -- synchronization
enforces a reentrant mutex, preventing more than one thread from
executing a block of code protected by a given monitor at the same time.
Unfortunately, most texts focus on the atomicity aspects of
synchronization to the exclusion of the other aspects. But
synchronization also plays a significant role in the JMM, causing the
JVM to execute memory barriers when acquiring and releasing monitors.
When a thread acquires a monitor, it executes a
read barrier --
invalidating any variables cached in thread-local memory (such as an
on-processor cache or processor registers), which will cause the
processor to re-read any variables used in the synchronized block from
main memory. Similarly, upon monitor release, the thread executes a
write barrier
-- flushing any variables that have been modified back to main memory.
The combination of mutual exclusion and memory
barriers means that as long as programs follow the correct
synchronization rules (that is, synchronize whenever writing a
variable that may next be read by another thread or when reading a
variable that may have been last written by another thread), each thread
will see the correct value of any shared variables it uses.
Some very strange things can happen if you fail to synchronize when
accessing shared variables. Some changes may be reflected across
threads instantly, while others may take some time (due to the nature of
associative caches). As a result, without synchronization you cannot be
sure that you have a consistent view of memory (related variables may
not be consistent with each other) or a current view of memory (some
values may be stale). The common -- and recommended -- way to avoid
these hazards is of course to synchronize properly. However, in some
cases, such as in very widely used library classes like
ConcurrentHashMap
,
it may be worth applying some extra expertise and effort in development
(which may well be many times as much effort) to achieve higher
performance.