
The Initialization-on-Demand Holder Idiom is a safe and efficient way to initialize static fields on demand in multithreading applications. It prevents subtle race conditions without impacting performance through complete synchronization of all accesses.
In this article you will find out:
- What is the motivation for the Initialization-on-Demand Holder Idiom?
- Why is complete synchronization with
synchronizednot optimal? - How is the Initialization-on-Demand Holder Idiom implemented in Java?
- What alternatives are there?
Motivation
For time-consuming initialization processes, it’s often advisable to perform them only when actually needed. For example, it might make sense to initialize a logger that establishes a connection to a database only when something is logged for the first time. This “Lazy Initialization” prevents unnecessary delays during program startup.
In a single-thread environment, the implementation is straightforward:
public class UserService {
private static Logger logger;
private static Logger getLogger() {
if (logger == null) {
logger = initializeLogger();
}
return logger;
}
public void createUser(User user) {
// . . .
getLogger().info("User created");
}
// . . .
}
The first call to getLogger() initializes the logger and stores it in the static field logger. Subsequent calls directly return the stored object.
However, this implementation is not thread-safe.
In a multithreading application, various subtle effects can occur here, which can lead to race conditions that are difficult to reproduce and thus hard to fix:
- With nearly simultaneous first calls to the
getLogger()method from two threads, both threads could see theloggerfield asnull, causing the logger to be initialized multiple times. - Due to cache effects, a thread could still see the
loggerfield asnulleven if another thread has already assigned it. - Due to instruction reordering, a thread could see the
loggerfield as notnull, but it could point to aLoggerobject that is not yet fully initialized.
I have described these three effects in detail in the article about the Double-Checked Locking Idiom.
In multithreading applications, we must therefore synchronize access to the logger field through appropriate measures.
Solution 1: Complete Synchronization
The simplest approach is complete synchronization of the getLogger() method using synchronized (or alternatively an explicit lock):
// ↓
private static synchronized Logger getLogger() {
if (logger == null) {
logger = initializeLogger();
}
return logger;
}
However, this implementation leads to significant performance losses because:
- every call requires the administrative overhead of complete synchronization,
- threads must wait during parallel accesses,
- entering and exiting the
synchronizedblock enforce memory barriers that ensure visibility between threads.
Due to this significant overhead, this solution is not optimal, especially for frequently called methods.
Solution 2: Double-Checked Locking
Another possible solution is the Double-Checked Locking mentioned above. I have explained this in detail in the linked article.
A correctly implemented Double-Checked Locking solves the performance problems mentioned above, but it is complicated and thus error-prone in implementation.
Solution 3: Initialization-on-Demand Holder
The third solution is the Initialization-on-Demand Holder Idiom. This also solves the performance problems mentioned above. It is easier to implement than Double-Checked Locking and thus less error-prone – but it only works with static fields, not with instance fields.
And this is how it is implemented:
public class UserService {
private static class LoggerHolder {
private static final Logger LOGGER = initializeLogger();
}
public void createUser(User user) {
// . . .
LoggerHolder.LOGGER.info("User created");
}
// . . .
}
Here, the Logger object is stored in the static LOGGER field of the inner class LoggerHolder.
But doesn’t this initialize the logger at program startup? Didn’t we want to avoid exactly that?
No, because the JVM initializes a class only when it is needed for the first time.
How does the Initialization-on-Demand Holder Idiom ensure Lazy Initialization?
When the JVM (Java Virtual Machine) loads the UserService class, it does not automatically initialize the LoggerHolder class with it. That happens only when LoggerHolder.LOGGER is accessed for the first time at runtime – and only then does the static initializer run that calls initializeLogger().
Whether the JVM loads the class earlier does not matter – loading does not run any initialization code yet.
Here’s a small demo program you can try out:
public class InitializationOnDemandHolderIdiomDemo {
private static class LoggerHolder {
private static final Logger LOGGER = initializeLogger();
private static Logger initializeLogger() {
System.out.println(">>>>>>>>>> Initializing logger <<<<<<<<<<");
return Logger.getLogger(
InitializationOnDemandHolderIdiomDemo.class.getName());
}
}
public static void main(String[] args) {
InitializationOnDemandHolderIdiomDemo demo =
new InitializationOnDemandHolderIdiomDemo();
demo.doSomethingWithoutLogging();
demo.doSomethingWithoutLogging();
demo.doSomethingWithoutLogging();
demo.doSomethingWithLogging();
demo.doSomethingWithLogging();
demo.doSomethingWithLogging();
}
private void doSomethingWithoutLogging() {
System.out.println("Not logging");
}
private void doSomethingWithLogging() {
System.out.println("\nI'm going to log something...");
LoggerHolder.LOGGER.info("Some log message");
System.out.println("Logged something");
}
}
You will see the following output:
Not logging
Not logging
Not logging
I'm going to log something...
>>>>>>>>>> Initializing logger <<<<<<<<<<
Jun 10, 2026 9:33:16 AM eu.happycoders.iodh.InitializationOnDemandHolderIdiomDemo doSomethingWithLogging
INFO: Some log message
Logged something
I'm going to log something...
Jun 10, 2026 9:33:16 AM eu.happycoders.iodh.InitializationOnDemandHolderIdiomDemo doSomethingWithLogging
INFO: Some log message
Logged something
I'm going to log something...
Jun 10, 2026 9:33:16 AM eu.happycoders.iodh.InitializationOnDemandHolderIdiomDemo doSomethingWithLogging
INFO: Some log message
Logged something
You see: The logger is initialized when – and only when – it is needed for the first time. With this, we can check off the requirement for “Lazy Initialization”.
And what about thread safety?
How does the Initialization-on-Demand Holder Idiom guarantee thread safety?
Thread safety is automatically guaranteed by the JVM when initializing classes.
This means that if two threads access LoggerHolder.LOGGER for the first time simultaneously, the JVM ensures that the LoggerHolder.LOGGER field is initialized only once – and also that both threads see the fully initialized Logger object.
The following diagram shows how the JVM achieves this: If two threads access LoggerHolder.LOGGER for the first time simultaneously, only one of them – Thread 1 here – runs the static initializer. Thread 2 waits until LoggerHolder is initialized (the dashed line) and only then reads the logger.
That almost sounds too good to be true...
Disadvantage of the Initialization-on-Demand Holder Idiom
As with almost everything, this solution also has a drawback:
If the call to the initializeLogger() method should fail during the first access to LoggerHolder.LOGGER, subsequent accesses will not attempt to call initializeLogger() again. No – once the initialization of a class has failed, the JVM will not try to initialize the class again. Instead, every subsequent access to LoggerHolder.LOGGER will immediately lead to a NoClassDefFoundError.
Here is a small program that demonstrates this behavior:
public class InitializationOnDemandHolderIdiomErrorDemo {
private static class LoggerHolder {
private static final Logger LOGGER = initializeLogger();
private static Logger initializeLogger() {
System.out.println(">>>>>>>>>> Initializing logger <<<<<<<<<<");
throw new RuntimeException("Initialization failed");
}
}
public static void main(String[] args) {
InitializationOnDemandHolderIdiomErrorDemo demo =
new InitializationOnDemandHolderIdiomErrorDemo();
demo.doSomethingWithLogging();
demo.doSomethingWithLogging();
demo.doSomethingWithLogging();
}
private void doSomethingWithLogging() {
try {
System.out.println("\nI'm going to log something...");
LoggerHolder.LOGGER.info("I did something smart");
System.out.println("Logged something");
} catch (Throwable t) {
System.out.println(">>>>>>>>>> " + t.getClass().getName() + " <<<<<<<<<<");
}
}
}
The program outputs the following:
I'm going to log something...
>>>>>>>>>> Initializing logger <<<<<<<<<<
>>>>>>>>>> java.lang.ExceptionInInitializerError <<<<<<<<<<
I'm going to log something...
>>>>>>>>>> java.lang.NoClassDefFoundError <<<<<<<<<<
I'm going to log something...
>>>>>>>>>> java.lang.NoClassDefFoundError <<<<<<<<<<
You see: Only on the first access to LoggerHolder.LOGGER is initializeLogger() called. All subsequent accesses lead directly to a NoClassDefFoundError.
With the other solutions – complete synchronization and double-checked locking – each subsequent call to the getLogger() method attempts to initialize the Logger object again.
Here’s a corresponding demo program for the variant with complete synchronization:
public class LazyInitializationErrorDemo {
private static Logger logger;
private static synchronized Logger getLogger() {
if (logger == null) {
logger = initializeLogger();
}
return logger;
}
private static Logger initializeLogger() {
System.out.println(">>>>>>>>>> Initializing logger <<<<<<<<<<");
throw new RuntimeException("Initialization failed");
}
public static void main(String[] args) {
LazyInitializationErrorDemo demo = new LazyInitializationErrorDemo();
demo.doSomethingWithLogging();
demo.doSomethingWithLogging();
demo.doSomethingWithLogging();
}
private void doSomethingWithLogging() {
try {
System.out.println("\nI'm going to log something...");
getLogger().info("I did something smart");
System.out.println("Logged something");
} catch (Throwable t) {
System.out.println(">>>>>>>>>> " + t.getClass().getName() + " <<<<<<<<<<");
}
}
}
And here’s the output of the program:
I'm going to log something...
>>>>>>>>>> Initializing logger <<<<<<<<<<
>>>>>>>>>> java.lang.RuntimeException <<<<<<<<<<
I'm going to log something...
>>>>>>>>>> Initializing logger <<<<<<<<<<
>>>>>>>>>> java.lang.RuntimeException <<<<<<<<<<
I'm going to log something...
>>>>>>>>>> Initializing logger <<<<<<<<<<
>>>>>>>>>> java.lang.RuntimeException <<<<<<<<<<
Here, with each call to getLogger(), there’s another attempt to initialize the logger.
Whether this is desired or not, of course, depends on the requirements.
The Three Solutions Compared
Before we look ahead, here are the three solutions side by side:
| Criterion | Complete synchronization | Double-Checked Locking | Initialization-on-Demand Holder |
|---|---|---|---|
| Performance | low – every access is synchronized | high – synchronizes only during initialization | high – no synchronization after the class is initialized |
| Implementation complexity | low | high and error-prone (requires volatile, among other things) | low and hard to get wrong |
| Applicable to | static and instance fields | static and instance fields | static fields only |
| Behavior on failed initialization | retried on every call | retried on every call (field stays null) | no retry – NoClassDefFoundError afterwards |
So if you want to lazily initialize a static field in a thread-safe and performant way, the Initialization-on-Demand Holder Idiom is usually the simplest choice. For instance fields, Double-Checked Locking remains the only option – with the additional effort described above – as long as the Lazy Constants described in the next section are a preview feature.
An Upcoming Alternative: Lazy Constants
The JDK developers are aware, too, that none of the existing solutions are ideal. That’s why a new feature is currently in the works: Lazy Constants.
Lazy Constants were introduced in Java 25 as a preview feature under the name Stable Values, then renamed to Lazy Constants and fundamentally redesigned in Java 26. A Lazy Constant is a container that encapsulates the thread-safe initialization of a constant on first access behind a simple API.
Here’s what that looks like with our logger example:
public class UserService {
private static final LazyConstant<Logger> LOGGER =
LazyConstant.of(UserService::initializeLogger);
public void createUser(User user) {
// . . .
LOGGER.get().info("User created");
}
// . . .
}
To the factory method LazyConstant.of(…), we merely pass the function that computes the value – the so-called computing function. This does not initialize the constant yet. Only the first call to LOGGER.get() invokes initializeLogger() and stores the result; every subsequent get() call returns the stored Logger object directly.
Here, too, initialization is thread-safe: even with concurrent access from multiple threads, the value is computed only once. Unlike the Holder Idiom, however, this is ensured not by the JVM’s class initialization but by the LazyConstant implementation itself.
And because LOGGER resides in a static final field, the JVM can treat its content as a true constant after initialization and optimize accordingly.
A Lazy Constant does not have to reside in a static field, though: Unlike the Holder Idiom, it also works in an instance field – just as lazy and thread-safe.
Since Java 27, Lazy Constants behave like the Holder Idiom when initialization fails: There is no second attempt. If the computing function throws an unchecked exception, the Lazy Constant transitions to an error state. get() then throws a NoSuchElementException with the original exception as its cause – and every subsequent get() call throws a NoSuchElementException again.
In Java 26, the Lazy Constant remained uninitialized in this case, and the next get() call retried the computation.
Lazy Constants are still a preview feature in Java 27 (JEP 531, third preview round) – you have to enable them with --enable-preview when compiling and running. At the time of writing, no follow-up JEP is scheduled for Java 28.
Conclusion
To initialize fields in multithreading applications only when needed, we can use the Double-Checked Locking Idiom or – for static fields – the Initialization-on-Demand Holder Idiom described in this article.
Both variants are more performant than complete synchronization of the access method with synchronized or an explicit lock.
However, Double-Checked Locking is complicated to implement and thus error-prone – all the more so since a faulty implementation is only revealed by race conditions, so usually not immediately, but potentially only after weeks or months.
The Holder Idiom is easier to implement, but it only works for static fields.
A variant that is performant, easy to implement, and also works for instance fields is currently in development: Lazy Constants (still in preview in Java 27).
What should I cover next? The best way for me to find out is through your feedback: a review on my ProvenExpert profile shows me which topics matter to you – and motivates me to write more articles.




