Singleton is a Creational Design Pattern that ensures a class has only one instance, and provides a global access point to this singleton instance.
Wikipedia says:
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to a singular instance. This pattern is useful when exactly one object is needed to coordinate actions across a system.
Key Characteristics:
Single Instance: a class has only one instance.
Global Access Point: provides a global access point to that instance.
Basic Structure:
Private Constructor: the constructor is made private so that the class cannot be instantiated from outside the class.
Private Static Field: a private static field instance holds the single instance of the class and should be created inside the class.
Public Static Method: a public static method getInstance(), commonly used as a global access point, is declared to return the singleton instance of the class.
Pros:
Has only one single instance.
Provides a single, globally accessible point to the instance.
Useful for managing shared resources.
Prevents frequent creation and destruction of instances, thereby reducing memory overhead.
Ensures consistency by using a single instance.
The instance can be created only when it’s first accessed based on Lazy Initialization.
Cons:
Inflexibility: it can make the code less flexible, as it ties the code to a specific instance.
Typically marked as final or designed in such a way that they cannot be subclassed.
Not suitable for mutable objects. If objects of the same type need to change in different use case scenarios, it can easily lead to data errors.
Violates the Single Responsibility Principle, if the design of the functionality is not reasonable.
Makes unit testing challenging.
Comparison of several implementations of Singleton Pattern:
// controls access to the singleton instance publicstatic LazySingletonThreadUnsafe getInstance() { if (null == instance) { /*try { // Simulate the scenario where multiple threads do not always get the same instance of a singleton, // increasing the probability of reproducing the issue. TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }*/ instance = newLazySingletonThreadUnsafe(); } return instance; } }
Testing Codes:
Single-threaded Environment: can get the same instance.
1 2 3 4 5 6 7 8 9
@Test publicvoidtestLazySingletonThreadUnsafeSingleThread() { LazySingletonThreadUnsafeinstance1= LazySingletonThreadUnsafe.getInstance(); LazySingletonThreadUnsafeinstance2= LazySingletonThreadUnsafe.getInstance(); System.out.println("instance1: " + instance1); System.out.println("instance2: " + instance2); // `instance1` and `instance2` are the same instance. Assertions.assertSame(instance1, instance2); // √ }
Multi-threaded Environment: may get different instances.
for (inti=0; i < THREAD_COUNT; i++) { futures[i] = executorService.submit(callableTask); }
LazySingletonThreadUnsafefirstInstance= futures[0].get(); for (Future<LazySingletonThreadUnsafe> future : futures) { LazySingletonThreadUnsafeinstance= future.get(); System.out.println(instance); // It may break the singleton pattern in a multi-threaded environment, resulting in different instances. // If the issue cannot be reproduced, try running the test method multiple times or uncommenting the code in LazySingletonThreadUnsafe.getInstance(). Assertions.assertSame(firstInstance, instance); // × }
executorService.shutdown(); }
1.2. Lazy Singleton - synchronized
1.1. Lazy Singleton - Thread Unsafe does not support multi-threading situations. We can simply and crudely add a lock with synchronized on the getInstance() method. While this approach can solve the thread safety issue, it is inefficient because it locks the class object (class-level lock, not object-level lock ) by locking the static method. This means that every time multiple threads try to access the singleton instance using the getInstance() method, it will be locked and only one thread can get access, greatly impacting performance. In fact, this method only needs to execute once for creating the instance of the class, and afterwards, all subsequent requests could directly return this instantiated object.
// synchronized static method: the class-level lock ensures that only one thread can execute the static synchronized method at a time. publicsynchronizedstatic LazySingletonSynchronized getInstance() { if (null == instance) { /*try { // Simulate the scenario where multiple threads do not always get the same instance of a singleton, // increasing the probability of reproducing the issue. TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }*/ instance = newLazySingletonSynchronized(); } return instance; } }
1.3. Lazy Singleton - Delay Lock
Based on 1.2. Lazy Singleton - synchronized, we can consider locking the instantiating process only when the instance returns at the first time. The instance is instantiated within the critical section, in order to ensure thread safety in a multi-threaded environment. Then, all subsequent operations will directly return the instance without entering the critical section, in order to improve multithreading performance.
BUT, there will still be thread safety issues in a multi-threaded environment. For example, in the following scenario, threads T1 and T2 may create two different instances.
T1: just enters the critical section and hasn’t instantiated yet: instance=null, T1 is holding the class-level lock
T2: enters the if code block, as the instance field hasn’t finished instantiation in T1 (instance is still null): instance=null, T1 is holding the class-level lock, T2 will be blocked before the critical section
T1: finishes instantiation and exits the critical section: instance=LazySingletonDelayLock@4a87761d (instance!=null), T1 released the lock, T2 has a chance to acquire the lock
T2: enters the critical section and executes instantiation again: instance=LazySingletonDelayLock@2db7a79b (Now it returns 2 different instances and breaks the singleton pattern!)
// delay lock in the if code block when `instance` is null publicstatic LazySingletonDelayLock getInstance() { if (null == instance) { // T2: may still enter if code block here, as the `instance` field hasn't finished instantiation in T1 (`instance` is still null) synchronized (LazySingletonDelayLock.class) { // class-level lock // T1: just enters the critical section and hasn't instantiated yet instance = newLazySingletonDelayLock(); } } return instance; } }
1.4. Lazy Singleton - Double-checked Locking Without volatile
To improve the issue of 1.3. Lazy Singleton - Delay Lock mentioned above, we further add a check within the synchronized block to verify whether the instance is null. If it is null, then proceed with instantiation. This helps prevent multiple instantiations caused by multi-threading scenarios.
However, due to the instance field not being marked as volatile, errors may still occur due to instruction reordering.
When creating a new object (such as instance = new LazySingletonNoVolatileDoubleCheck()), there are essentially three main steps involved internally: 1. space allocation → 2. initialization → 3. reference assignment.
The JVM allows for reordering of instructions for performance reasons, while these three steps may be reordered, such as 1. space allocation → 3. reference assignment → 2. initialization, it can potentially lead to thread safety issues:
T1: enters the critical section and ready to new
T1: space allocation
T1: reference assignment (Since initialization is not yet complete, although instance now points to the newly allocated heap space, the object is only half-initialized.)
T2: In the first if check, when instance is found to be non-null (T1 has not finished full initialization yet), returning the partially initialized object directly leads to program errors.
// add double-checked locking without volatile `instance` publicstatic LazySingletonNoVolatileDoubleCheck getInstance() { if (null == instance) { // T2: directly returns the `instance`, but not fully initialized, leading to a partially constructed or uninitialized instance being returned synchronized (LazySingletonNoVolatileDoubleCheck.class) { if (null == instance) { instance = newLazySingletonNoVolatileDoubleCheck(); // bytecode: 1. space allocation -> 2. initialization -> 3. reference assignment // may reorder instructions as follows: // 1. space allocation // 3. reference assignment // T1: the `instance` is assigned the reference, but not fully initialized // 2. initialization } } } return instance; } }
We already find out that there are so many steps inside creating a new object. So, Let’s delve into understanding the creation of a new object from the perspective of Java bytecode. Let’s start by writing a simple test code for creating an object as shown below:
We can compile NewTest.java to get NewTest.class via javac -g NewTest.java. Then, we can execute javap -v -p NewTest.class to get the bytecode information about the fields, constructors, and methods of a class file.
Classfile /DesignPattern-Lab/src/main/java/com/sissilab/dp/ox1_creational/ox11_singleton/NewTest.class Last modified 25/07/2024; size 487 bytes MD5 checksum e132b23dc7ec6394be9137ad5aa7c33a Compiled from "NewTest.java" publicclasscom.sissilab.dp.ox1_creational.ox11_singleton.NewTest minor version: 0 major version: 52// Java major version, 52 indicates Java classes compiled with JDK 1.8 flags: ACC_PUBLIC, ACC_SUPER // class access modifiers: the class is public Constant pool: #1 = Methodref #4.#19// java/lang/Object."<init>":()V #2 = Class #20// com/sissilab/dp/ox1_creational/ox11_singleton/NewTest #3 = Methodref #2.#19// com/sissilab/dp/ox1_creational/ox11_singleton/NewTest."<init>":()V #4 = Class #21// java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = Utf8 Code #8 = Utf8 LineNumberTable #9 = Utf8 LocalVariableTable #10 = Utf8 this #11 = Utf8 Lcom/sissilab/dp/ox1_creational/ox11_singleton/NewTest; #12 = Utf8 main #13 = Utf8 ([Ljava/lang/String;)V #14 = Utf8 args #15 = Utf8 [Ljava/lang/String; #16 = Utf8 newTest #17 = Utf8 SourceFile #18 = Utf8 NewTest.java #19 = NameAndType #5:#6// "<init>":()V #20 = Utf8 com/sissilab/dp/ox1_creational/ox11_singleton/NewTest #21 = Utf8 java/lang/Object { public com.sissilab.dp.ox1_creational.ox11_singleton.NewTest(); // auto-generated default no-argument constructor descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1// Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 050this Lcom/sissilab/dp/ox1_creational/ox11_singleton/NewTest;
publicstaticvoidmain(java.lang.String[]); // main method descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 // new: allocates memory for an object of the class referenced by #2 -> adds the reference to the top of the operand stack 0: new #2// class com/sissilab/dp/ox1_creational/ox11_singleton/NewTest // dup: duplicates the top stack value -> adds the duplicated reference to the top of the operand stack (because the subsequent `invokespecial` will consume a reference.) 3: dup // invokespecial: calls the constructor to initialize the newly opened space and consume a reference 4: invokespecial #3// Method "<init>":()V // astore_1: pops the top stack value -> assigns the reference to the element at position 1 (`newTest`) in the LocalVariableTable 7: astore_1 8: return LineNumberTable: // maps the bytecode instructions to the lines in the source file line 8: 0 line 9: 8 LocalVariableTable: Start Length Slot Name Signature 090 args [Ljava/lang/String; 811 newTest Lcom/sissilab/dp/ox1_creational/ox11_singleton/NewTest; } SourceFile: "NewTest.java"
Although just a few simple lines of source code, the decompiled content is extensive. Focus mainly on the Code section below:
1 2 3 4
0: new #2// class com/sissilab/dp/ox1_creational/ox11_singleton/NewTest 3: dup 4: invokespecial #3// Method "<init>":()V 7: astore_1
The process is illustrated below:
The reason why the dup instruction is needed to duplicate the top element of the stack is because the next step, invokespecial, needs to execute the default constructor, which will pop a reference from the top of the stack. If there is no dup to duplicate the top of the stack, it would result in an empty stack after invokespecial, causing the newly created object to be lost.
This explains why new is not an atomic operation. Although in Java code it is just one line, at the bytecode level, it translates to key four instruction operations. Due to the instruction reordering, the three steps inside new can be reordered. In a multi-threaded environment, this can lead to issues in the singleton pattern mentioned above. Therefore, it’s necessary to add volatile to the instance field to prevent reordering.
Double Checked Locking is the most recommended lazy initialization singleton pattern, which ensures thread safety and enables lazy loading. The complete code is as follows:
/** * 1.5. Lazy Singleton - Double-checked Locking (DCL) * * Pros: * 1. lazy initialization * 2. can guarantee thread safety using double-checked locking (DCL) * 3. can reduce locking overhead * <p> * Cons: * 1. more complex * 2. may influence its performance when facing a large number of initial `getInstance()` requests * <p> * Best Use Cases: * 1. Use it when the singleton class holds heavy resources. * 2. Use it when we want to delay the creation of the singleton instance until it is actually needed. * 3. Use it when we consider both performance and lazy initialization. * 4. Avoid it when the singleton class is lightweight or always needed. */ classLazySingletonDoubleCheck {
// holds the singleton instance: add volatile to prevent reordering privatestaticvolatile LazySingletonDoubleCheck instance;
1.5.1. Reflection Attack on ‘Lazy Singleton - DCL’
Typically, to avoid reflection attacks, we usually add a check in the private constructor to see if the instance field is null. If the instance is not null, we can assume that this singleton class has already created its object and throw an exception to prevent the reflection attack.
However, it is vulnerable to reflection attacks, which can break the intended singleton behaviour. Let’s see the following two cases:
case 1 (√): If we call getInstance() first and then call constructor.newInstance(), an error will be thrown to prevent reflection attacks.
case 2 (×): If we call constructor.newInstance() first and then call getInstance(), we will get two different instances leading to break singleton pattern.
So, Lazy Singleton cannot prevent reflection attacks in all cases. The specific reasons are:
Reflection allows access to private constructors. An attacker can use reflection mechanism to bypass the private constructor and create a new instance of a singleton class.
It lacks any inherent mechanism to check if the instance of a singleton class already exists when accessed via reflection.
Throwing an exception in the private constructor to prevent reflection attacks is not a foolproof plan, as an attacker can still manipulate the instance creation process.
// holds the singleton instance: add volatile to prevent reordering privatestaticvolatile LazySingletonDoubleCheckReflectionProof instance;
// private constructor: restricts users from creating instances themselves, but cannot prevent constructor reflection privateLazySingletonDoubleCheckReflectionProof() { // cannot prevent reflection attack: the first call using reflection will still succeed, but subsequent attempts will fail. if (null != instance) { thrownewIllegalStateException("This singleton instance already exists."); } }
1.5.2. Serialization Attack on ‘Lazy Singleton - DCL’
Serializing and deserializing a class need to implement java.io.Serializable, otherwise it’ll throw an java.io.NotSerializableException. The general process of serialization and deserialization:
Serializing the singleton class to generate a serialized file.
Reading the serialized file to obtain the instance of the singleton class via deserialization.
Comparing whether the serialized instance and the deserialized instance are the same.
// The class has to implement `java.io.Serializable` to avoid throwing `java.io.NotSerializableException`. classLazySingletonDoubleCheckSerializationimplementsSerializable {
// holds the singleton instance: add volatile to prevent reordering privatestaticvolatile LazySingletonDoubleCheckSerialization instance;
First, let’s serialize the singleton class to generate a serialized file -> read the serialized file and obtain the instance of the singleton class via deserialization -> the deserialized instance is not the same as the instance of getInstance(), thereby breaking the singleton pattern. Here is the testing code:
// deserialize: the constructor will not be called, the data will be initialized by reading from the byte stream. LazySingletonDoubleCheckSerialization testSerializableInstance; try (ObjectInputStreamobjectInputStream=newObjectInputStream(Files.newInputStream(serializedClassPath))) { testSerializableInstance = ((LazySingletonDoubleCheckSerialization) objectInputStream.readObject()); } catch (IOException | ClassNotFoundException e) { thrownewRuntimeException(e); }
The reason why the above deserialization generated a new object is that serialization has its own mechanism. It reads the byte stream data and does not call the constructor. For non-Enum Singleton, to prevent serialization attacks, you need to add a version number like private static final long serialVersionUID = -1L;, and also implement the readResolve() method, and return the instance of the class. In this way, you can get the same object. The code is as follows:
/** * It is mainly used for version control, ensuring compatibility between serialized and deserialized class versions. * If the `serialVersionUID` is not added during serialization, a version number will be automatically generated based on the current class data. * When deserializing, a version number will also be automatically generated. * If the two version numbers do not match (e.g. the class content is modified before deserialization), the following error will be reported: * `java.io.InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -4190675925334731018, local class serialVersionUID = 1437610846000386433 * Specifying a serialVersionUID can solve this problem. */ privatestaticfinallongserialVersionUID= -1L;
// holds the singleton instance: add volatile to prevent reordering privatestaticvolatile LazySingletonDoubleCheckSerializationProof instance;
// private constructor: restricts users from creating instances themselves. privateLazySingletonDoubleCheckSerializationProof() { // cannot prevent reflection attacks in all cases if (null != instance) { thrownewIllegalStateException("This singleton instance already exists."); } }
// use double-checked locking publicstatic LazySingletonDoubleCheckSerializationProof getInstance() { if (null == instance) { synchronized (LazySingletonDoubleCheckSerializationProof.class) { if (null == instance) { instance = newLazySingletonDoubleCheckSerializationProof(); } } } return instance; }
/** * For non-Enum Singleton pattern, when facing serialization attacks, the `readResolve()` method must be implemented. * * @return * @throws ObjectStreamException */ Object readResolve()throws ObjectStreamException { // during deserialization: returning the existing instance, instead of creating a new one. return getInstance(); } }
Let’s analyse deserialization source code: objectInputStream.readObject() -> readObject(Object.class) -> Object obj = readObject0(type, false); -> in the case-when for TC_OBJECT:readOrdinaryObject(unshared).
The key points of readOrdinaryObject(unshared) are:
if (obj != null && handles.lookupException(passHandle) == null && // hasReadResolveMethod:check if the current class has the `readResolve()` method. desc.hasReadResolveMethod()) { // Calling the `readResolve()` method of the current class. Objectrep= desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } // The `rep` returned by `readResolve()` is assigned to the `obj`. handles.setObject(passHandle, obj = rep); } }
return obj; }
booleanhasReadResolveMethod() { requireInitialized(); // if the `readResolveMethod` is not null, it means the current class has the `readResolve()` method. return (readResolveMethod != null); }
// get the `readResolve()` method of the current class and assign it to the `readResolveMethod` variable readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class); // ... }
/** * Lazy Singleton - Double-checked Locking (DCL): Perfect Version * 1. Using volatile + DCL to guarantee thread safety and minimize the use of synchronization * 2. Trying to use the private constructor checks to prevent reflection attacks, but cannot guarantee in all cases * 3. Using defined serialVersionUID and readResolve() to prevent serialization attacks * 4. Overriding clone() to prevent cloning attacks * 5. Using final class to prevent subclassing and avoid any subclass to break the singleton pattern */ publicfinalclassLazySingletonDoubleCheckPerfectimplementsSerializable {
// holds the singleton instance: add volatile to prevent reordering privatestaticvolatile LazySingletonDoubleCheckPerfect instance;
// private constructor: restricts users from creating instances themselves. privateLazySingletonDoubleCheckPerfect() { // cannot prevent reflection attacks in all cases if (null != instance) { thrownewIllegalStateException("This singleton instance already exists."); } }
// use double-checked locking publicstatic LazySingletonDoubleCheckPerfect getInstance() { if (null == instance) { synchronized (LazySingletonDoubleCheckPerfect.class) { if (null == instance) { instance = newLazySingletonDoubleCheckPerfect(); } } } return instance; }
// Prevent serialization attacks private Object readResolve()throws ObjectStreamException { // during deserialization: returning the existing instance, instead of creating a new one. return getInstance(); }
// Prevent cloning @Override protected Object clone()throws CloneNotSupportedException { // (1) throw an exception to prevent cloning thrownewCloneNotSupportedException("Cloning of this singleton instance is not allowed"); // (2) or directly return the same instance from the clone method //return getInstance(); } }
2. Eager Singleton
The eager Initialization pattern completes initialization when the class is loaded, relying on the class loading mechanism to ensure singleton behaviour without locking, thus achieving higher execution efficiency. However, due to instantiation during class loading, it lacks the advantage of lazy loading. If the instance is not used, it will be needlessly instantiated, wasting resources.
In the following two Eager Singleton examples, both are very simpler than the Lazy Singleton. These two patterns leverage the JVM’s class loading mechanism to ensure the uniqueness of the instance. Initialization occurs only once, and the JVM synchronizes the class loading process.
The class loading process involves:
Loading: the JVM locates and loads the class file’s bytecode into memory, generating the corresponding Class data structure.
Linking: Verification (verifying the correctness of the loaded bytecode) → Preparation (initializing static fields to their default values) → Resolution (resolving symbolic references from the class file to actual references)
Initialization: the static initializers and static blocks in the class are executed inside <clinit> and this process only happens the first time the class is used, such as an instance is created or a static method is invoked.
Using: After a class is loaded and initialized, its instances can be created, methods can be invoked, and fields can be accessed.
Unloading: When a class and its associated class loader are no longer in use and can be garbage collected, the class is removed from memory.
As instance variable is a static member variable, its instantiation occurs during the class loading initialization phase. This phase involves executing the class constructor <clinit>() method, which the compiler automatically collects all static variables and static blocks within. Therefore, private static final EagerSingletonStaticConstant instance = new EagerSingletonStaticConstant(); and static { instance = new EagerSingletonStaticBlock(); } also happen within this method. The JVM ensures that <clinit>() of a class is synchronized in a multi-threaded environment, guaranteeing thread safety.
Tip
Some people may feel confused about why Eager Singleton is more wasting resources. They may think: ‘When that Eager Singleton class is never used, it appears to not invoke the constructor of the class’.
Actually, it is more of this kind of scenario, calling other methods of the class, rather than calling the getInstance(). For Eager Singleton, even if you don’t get the instance of the Eager Singleton and just call any other methods, it’ll still trigger the class creation due to the class loading mechanism. But for Lazy Singleton, it won’t trigger class creation and just execute that other method.
It’s very simple to implement, but it may lead to wasting resources as the instance of the class is always created, whether it is required or not. It is also not easy to handle exception during the class creation.
/** * 2.1. Eager Singleton - static constant * <p> * Pros: * 1. can guarantee the creation is thread-safe based on class loading mechanism without the need for synchronization * 2. more straightforward and easy to understand, and no need for additional code to handle lazy initialization or synchronization mechanisms * 3. no performance overhead during `getInstance()`, as the instance is created at the time of class loading * <p> * Cons: * 1. may waste resources due to prematurely creation, especially for the resource-heavy singleton classes with large caches or database connections * 2. lack of lazy initialization * 3. inflexible to handle exceptions during the class creation using static constant * <p> * Best Use Cases: * 1. Use it when the singleton class is relatively lightweight. * 2. Use it when the singleton class is frequently needed during the application's lifecycle. * 3. Avoid it when lazy loading is preferable. */ classEagerSingletonStaticConstant {
// holds the singleton instance: initialized when loading the class privatestaticfinalEagerSingletonStaticConstantinstance=newEagerSingletonStaticConstant();
// holds the singleton instance: initialized when loading the class privatestaticfinalEagerSingletonStaticConstantReflectionProofinstance=newEagerSingletonStaticConstantReflectionProof();
// holds the singleton instance: initialized when loading the class privatestaticfinalEagerSingletonStaticConstantSerializationProofinstance=newEagerSingletonStaticConstantSerializationProof();
// controls access to the singleton instance publicstatic EagerSingletonStaticConstantSerializationProof getInstance() { return instance; }
Object readResolve()throws ObjectStreamException { // during deserialization: returning the existing instance, instead of creating a new one. return getInstance(); } }
2.1.3. Perfect Version of Eager Singleton - static constant
/** * Eager Singleton - static constant: Perfect Version * 1. Using static constant to create the singleton instance based on class loading mechanism * 2. Trying to use the private constructor checks to prevent reflection attacks, but cannot guarantee in all cases * 3. Using defined serialVersionUID and readResolve() to prevent serialization attacks * 4. Overriding clone() to prevent cloning attacks * 5. Using final class to prevent subclassing and avoid any subclass to break the singleton pattern */ publicfinalclassEagerSingletonStaticConstantPerfectimplementsSerializable {
// holds the singleton instance: initialized when loading the class privatestaticfinalEagerSingletonStaticConstantPerfectinstance=newEagerSingletonStaticConstantPerfect();
// private constructor: restricts users from creating instances themselves. privateEagerSingletonStaticConstantPerfect() { // cannot prevent reflection attacks in all cases if (null != instance) { thrownewIllegalStateException("This singleton instance already exists."); } }
// controls access to the singleton instance publicstatic EagerSingletonStaticConstantPerfect getInstance() { return instance; }
// Prevent serialization attacks private Object readResolve()throws ObjectStreamException { // during deserialization: returning the existing instance, instead of creating a new one. return getInstance(); }
// Prevent cloning @Override protected Object clone()throws CloneNotSupportedException { // (1) throw an exception to prevent cloning thrownewCloneNotSupportedException("Cloning of this singleton instance is not allowed"); // (2) or directly return the same instance from the clone method //return getInstance(); } }
2.2. Eager Singleton - static block
Using static block to finish class instantiation may also lead to wasting resources due to class loading mechanism. But it’s easier to handle exception and do more things inside the static block.
/** * 2.2. Eager Singleton - static block * <p> * Pros: * 1. can guarantee the creation is thread-safe based on class loading mechanism without the need for synchronization * 2. more straightforward and easy to understand, and no need for additional code to handle lazy initialization or synchronization mechanisms * 3. no performance overhead during `getInstance()`, as the instance is created at the time of class loading * 4. flexible to handle more complex logic or exception in static block during the creation * <p> * Cons: * 1. may waste resources due to prematurely creation, especially for the resource-heavy singleton classes with large caches or database connections * 2. lack of lazy initialization * <p> * Best Use Cases: * 1. Use it when the singleton class is relatively lightweight. * 2. Use it when the singleton class is frequently needed during the application's lifecycle. * 3. Use it when we require complex initialization steps, as the static block gives flexibility to handle. * 4. Avoid it when lazy loading is preferable. */ classEagerSingletonStaticBlock {
// holds the singleton instance: initialized when loading the class privatestaticfinal EagerSingletonStaticBlock instance;
// holds the singleton instance: initialized when loading the class in the static block privatestaticfinal EagerSingletonStaticBlockReflectionProof instance;
// holds the singleton instance: initialized when loading the class in the static block privatestaticfinal EagerSingletonStaticBlockSerializationProof instance;
// controls access to the singleton instance publicstatic EagerSingletonStaticBlockSerializationProof getInstance() { return instance; }
// prevent serialization attack Object readResolve()throws ObjectStreamException { // during deserialization: returning the existing instance, instead of creating a new one. return getInstance(); } }
2.2.3. Perfect Version of Eager Singleton - static block
/** * Eager Singleton - static block: Perfect Version * 1. Using static block to create the singleton instance based on class loading mechanism * 2. Trying to use the private constructor checks to prevent reflection attacks, but cannot guarantee in all cases * 3. Using defined serialVersionUID and readResolve() to prevent serialization attacks * 4. Overriding clone() to prevent cloning attacks * 5. Using final class to prevent subclassing and avoid any subclass to break the singleton pattern */ publicfinalclassEagerSingletonStaticBlockPerfectimplementsSerializable {
// private constructor: restricts users from creating instances themselves. privateEagerSingletonStaticBlockPerfect() { // cannot prevent reflection attacks in all cases if (null != instance) { thrownewIllegalStateException("This singleton instance already exists."); } }
// controls access to the singleton instance publicstatic EagerSingletonStaticBlockPerfect getInstance() { return instance; }
// Prevent serialization attacks private Object readResolve()throws ObjectStreamException { // during deserialization: returning the existing instance, instead of creating a new one. return getInstance(); }
// Prevent cloning @Override protected Object clone()throws CloneNotSupportedException { // (1) throw an exception to prevent cloning thrownewCloneNotSupportedException("Cloning of this singleton instance is not allowed"); // (2) or directly return the same instance from the clone method //return getInstance(); } }
3. Static Inner Class Singleton
The Static Inner Class Singleton pattern essentially also utilises the classloader mechanism to ensure thread safety. Due to its characteristics, it can guarantee that the class initialization will only be triggered when it is actually used, which is also a form of lazy loading.
Compared to 2. Eager Singleton, this pattern also utilises classloader mechanism to initialize and ensure only one instance of the class. When the class is loaded, the Eager Singleton will be instantiated, but the Static Inner Class Singleton may not be. This is because the instance will only be created when its static inner class InstanceHolder is used and loaded. So, the instance of StaticInnerClassSingleton won’t be created until the method getInstance() has been used, which can achieve the effect of lazy loading.
/** * 3. Static Inner Class Singleton * <p> * Pros: * 1. lazy initialization based on the characteristics of static inner class * 2. can guarantee thread safety for the static initialization of the `InstanceHolder` class based on class loading mechanism * 3. can avoid synchronization overhead, unlike the DCL pattern * 4. relatively easier to understand compared to the DCL pattern * <p> * Cons: * 1. inflexible to handle exceptions during the class creation * 2. unexpected issues due to class loading timing * <p> * Best Use Cases: * 1. Use it when the singleton class holds heavy resources. * 2. Use it when we want to delay the creation of the singleton instance until it is actually needed. * 3. Avoid it when we require complex initialization steps */ publicclassStaticInnerClassSingleton {
// static inner class to provide the singleton instance of the class privatestaticclassInstanceHolder { privatestaticStaticInnerClassSingletoninstance=newStaticInnerClassSingleton(); }
// controls access to the singleton instance publicstatic StaticInnerClassSingleton getInstance() { // It is only here that the initialization of `InstanceHolder.instance` will be triggered return InstanceHolder.instance; } }
3.1. Reflection Attack on Static Inner Class Singleton
// static inner class to provide the singleton instance of the class privatestaticclassInstanceHolder { privatestaticStaticInnerSingletonReflectionProofinstance=newStaticInnerSingletonReflectionProof(); }
// controls access to the singleton instance publicstatic StaticInnerSingletonReflectionProof getInstance() { // It is only here that the initialization of `InstanceHolder.instance` will be triggered. return InstanceHolder.instance; } }
3.2. Serialization Attack on Static Inner Class Singleton
/** * If the `serialVersionUID` is not added during serialization, a version number will be automatically generated based on the current class data. * When deserializing, a version number will also be automatically generated. * If the two version numbers do not match (the class content is modified before deserialization), the following error will be reported: * `java.io.InvalidClassException: local class incompatible: stream classdesc serialVersionUID = -6193354024038071874, local class serialVersionUID = 3430229600994946531` * Specifying a serialVersionUID can solve this problem. */ privatestaticfinallongserialVersionUID= -1L;
// static inner class to provide the singleton instance of the class privatestaticclassInstanceHolder { privatestaticStaticInnerSingletonSerializationProofinstance=newStaticInnerSingletonSerializationProof(); }
// controls access to the singleton instance publicstatic StaticInnerSingletonSerializationProof getInstance() { return InstanceHolder.instance; }
/** * For non-Enum Singleton pattern, when facing serialization attacks, the `readResolve()` method must be implemented. * * @return * @throws ObjectStreamException */ Object readResolve()throws ObjectStreamException { // during deserialization: returning the existing instance, instead of creating a new one. return getInstance(); } }
3.3. Perfect Version of Static Inner Class Singleton
/** * Static Inner Class Singleton: Perfect Version * 1. Using the characteristics of static inner class to provide lazy initialization and thread safety based on class loading mechanism * 2. Trying to use the private constructor checks to prevent reflection attacks, but cannot guarantee in all cases * 3. Using defined serialVersionUID and readResolve() to prevent serialization attacks * 4. Overriding clone() to prevent cloning attacks * 5. Using final class to prevent subclassing and avoid any subclass to break the singleton pattern */ publicfinalclassStaticInnerSingletonPerfectimplementsSerializable {
// static inner class to provide the singleton instance of the class privatestaticclassInstanceHolder { privatestaticStaticInnerSingletonPerfectinstance=newStaticInnerSingletonPerfect(); }
// controls access to the singleton instance publicstatic StaticInnerSingletonPerfect getInstance() { return StaticInnerSingletonPerfect.InstanceHolder.instance; }
// prevent serialization attacks Object readResolve()throws ObjectStreamException { // during deserialization: returning the existing instance, instead of creating a new one. return getInstance(); }
// Prevent cloning @Override protected Object clone()throws CloneNotSupportedException { // (1) throw an exception to prevent cloning thrownewCloneNotSupportedException("Cloning of this singleton instance is not allowed"); // (2) or directly return the same instance from the clone method //return getInstance(); } }
4. Enum Singleton
Enum is considered the best way to implement the singleton pattern. It not only leverages the class loading mechanism to guarantee thread safety, but also prevents the reconstruction of objects during deserialization, and avoids reflection attacks.
/** * 4. Enum Singleton * <p> * Pros: * 1. very easy to implement and the code is concise. * 2. inherently thread-safe as classloader mechanism, private constructor, and public static final instance field. * 3. can prevent reflection attacks because it'll throw an IllegalArgumentException while calling `newInstance()`. * 4. inherently serializable and can prevent serialization attacks. * <p> * Cons: * 1. no lazy loading. * 2. cannot extend other classes because an enum class implicitly inherits `java.lang.Enum`. * 3. inconvenient to debug because some key codes are implicitly auto-generated, like private constructor, static initialization block, ... */ publicenumEnumSingleton { instance;
publicvoidanyMethod() { } }
Let’s analyse the bytecode of the enum class. After compiling the EnumSingleton.java and generating the class file through javac -g EnumSingleton.java, executing javap -v -p EnumSingleton.class to generate the bytecode file, as follows:
// the `private static final $VALUES` field holds all enum constants privatestaticfinal com.sissilab.dp.ox1_creational.ox11_singleton.EnumSingleton[] $VALUES; descriptor: [Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton; flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL, ACC_SYNTHETIC
// `values()` method is automatically generated by the compiler to return the array of defined enum constants. publicstatic com.sissilab.dp.ox1_creational.ox11_singleton.EnumSingleton[] values(); descriptor: ()[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton; flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #1// Field $VALUES:[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton; 3: invokevirtual #2// Method "[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;".clone:()Ljava/lang/Object; 6: checkcast #3// class "[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;" 9: areturn LineNumberTable: line 17: 0
publicstatic com.sissilab.dp.ox1_creational.ox11_singleton.EnumSingleton valueOf(java.lang.String); descriptor: (Ljava/lang/String;)Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton; flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: ldc #4// class com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton 2: aload_0 3: invokestatic #5// Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum; 6: checkcast #4// class com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton 9: areturn LineNumberTable: line 17: 0 LocalVariableTable: Start Length Slot Name Signature 0100 name Ljava/lang/String;
// The private constructor with two arguments (String, int) is automatically generated. private com.sissilab.dp.ox1_creational.ox11_singleton.EnumSingleton(); descriptor: (Ljava/lang/String;I)V flags: ACC_PRIVATE Code: stack=3, locals=3, args_size=3 // Loads the `this` reference onto the stack (from slot 0). 0: aload_0 // Loads the first argument (`name` of the enum constant) stored in local variable 1 onto the stack. 1: aload_1 // Loads the second argument (`ordinal` of the enum constant) stored in local variable 2 onto the stack. 2: iload_2 // calls superclass constructor `java.lang.Enum (String name, int ordinal)` 3: invokespecial #6// Method java/lang/Enum."<init>":(Ljava/lang/String;I)V 6: return LineNumberTable: line 17: 0 LocalVariableTable: Start Length Slot Name Signature 070this Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton; Signature: #28// ()V
// static initialization block static {}; descriptor: ()V flags: ACC_STATIC Code: stack=4, locals=0, args_size=0 // allocates memory for an object of the class referenced by #4 -> adds the reference to the top of the operand stack 0: new #4// class com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton // dup: duplicates the top stack value -> adds the duplicated reference to the top of the operand stack 3: dup // ldc: loads String instance onto the stack (`String name`) 4: ldc #7// String instance // iconst_0: pushes the integer constant 0 onto the stack (`int ordinal`) 6: iconst_0 // invokespecial:invokes the constructor EnumSingleton(String, int) to initialize 7: invokespecial #8// Method "<init>":(Ljava/lang/String;I)V // putstatic: assigns the created instance to the static field `instance` of EnumSingleton 10: putstatic #9// Field instance:Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton; // iconst_1: pushes the integer 1 (the size of the array to be created) onto the stack. 13: iconst_1 // anewarray: creates a new array of EnumSingleton objects (size=1) 14: anewarray #4// class com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton // dup: duplicates the reference to the array on the stack 17: dup // iconst_0: pushes the integer 0 (the index of the array) onto the stack 18: iconst_0 // getstatic: get static field `instance` 19: getstatic #9// Field instance:Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton; // aastore: stores the instance into the array at index 0 22: aastore // putstatic: stores the array into the static field `$VALUES` (it holds all the enum constants in the order) 23: putstatic #1// Field $VALUES:[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton; 26: return LineNumberTable: line 18: 0 line 17: 13 } Signature: #31// Ljava/lang/Enum<Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;>; SourceFile: "EnumSingleton.java"
In fact, the initialization process happens in the static initialization block (static {}, it’ll be executed when the class is first loaded.), the key code is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// allocates memory for an object of the class referenced by #4 -> adds the reference to the top of the operand stack 0: new #4// class com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton
// dup: duplicates the top stack value -> adds the duplicated reference to the top of the operand stack 3: dup
// iconst_0: pushes the integer constant 0 onto the stack 6: iconst_0
// invokespecial:invokes the constructor EnumSingleton(String, int) to initialize 7: invokespecial #8// Method "<init>":(Ljava/lang/String;I)V
// putstatic: assigns the created instance to the static field `instance` of EnumSingleton 10: putstatic #9// Field instance:Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
4.1. Reflection Attack on Enum Singleton
When attempting to obtain the instance of an enum class through reflection, an error will be thrown: java.lang.IllegalArgumentException: Cannot reflectively create enum objects. This effectively prevents reflection attacks
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@Test publicvoidtestReflectionAttackOnEnumSingleton()throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { // get the constructor: only automatically generate a private constructor with two parameters, like `private EnumSingleton(String, int)` Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class); // set the accessibility of the constructor constructor.setAccessible(true); // reflectInstance: create the instance through constructor EnumSingletonreflectInstance= constructor.newInstance("instance", 0); // java.lang.IllegalArgumentException: Cannot reflectively create enum objects
// instance: get the instance through the normal public static final `instance` field EnumSingletoninstance= EnumSingleton.instance;
In fact, the error is thrown during public T newInstance(Object ... initargs). The following source code indicates that there is an explicit check about whether current class is enum type and the IllegalArgumentException will be thrown if yes. So, Enum Singleton can prevent reflection attacks.
@CallerSensitive public T newInstance(Object ... initargs)throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } // If current class is enum type, the `IllegalArgumentException` will be thrown. if ((clazz.getModifiers() & Modifier.ENUM) != 0) thrownewIllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessorca= constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") Tinst= (T) ca.newInstance(initargs); return inst; }
// ... }
4.2. Serialization Attack on Enum Singleton
Every enum classes implicitly inherit from java.lang.Enum abstract class that implements java.io.Serializable, which means enum classes can support serialization and deserialization. Thanks to its built-in mechanisms, Enum Singleton is inherently safe from serialization attacks, for no new instances of enum constants are created during deserialization.
Let’s see what happened about Enum Singleton in deserialization source code. Within readObject0(type, false) method, in the case-when for TC_ENUM:–> readEnum(boolean unshared):
private Enum<?> readEnum(boolean unshared) throws IOException { // only objects serialized as enum are processed if (bin.readByte() != TC_ENUM) { thrownewInternalError(); }
ObjectStreamClassdesc= readClassDesc(false); // If the class isn't an enum, an InvalidClassException will be thrown. if (!desc.isEnum()) { thrownewInvalidClassException("non-enum class: " + desc); }
// get the name of the enum constant, e.g. `instance` (Each constant name must be unique within an enum.) Stringname= readString(false); Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") // find the constant using the enum class (e.g. `EnumSingleton`) and the constant name (e.g. `instance`) // It's the key part that no new enum instances are created, regardless of how many times the enum is serialized and deserialized. Enum<?> en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) newInvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { // In the default mode (Shared Mode), the `unshared` is false. This means that if an object appears multiple times in a serialized stream, all references to that object will point to the same deserialized instance. // The existing handle (`Object[] entries`) will be updated, which can ensure that the same enum instance is reused, especially for Enum Singleton. handles.setObject(enumHandle, result); } }
publicstatic <T extendsEnum<T>> T valueOf(Class<T> enumType, String name) { // get the enum constant by its name from an internal map (`Map<String, T> enumConstantDirectory`) // There's a pre-built internal map within `enumConstantDirectory()` that contains all enum constants loaded into memory, which ensures the uniqueness of each enum constant. Tresult= enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) thrownewNullPointerException("Name is null"); thrownewIllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); }
/** * Use this singleton: * `EnumSingletonPerfect.instance.anyMethod();` */ publicenumEnumSingletonPerfect { instance;
publicvoidanyMethod() { }
/** * Access the lazy-loaded resource: * `EnumSingletonPerfect.instance.getExpensiveResource().useResource();` */ // Using a static inner class to implement lazy loading of heavy resources privatestaticclassLazyHeavyResource { privatestaticfinalHeavyResourceRESOURCE=newHeavyResource(); }
public HeavyResource getExpensiveResource() { return LazyHeavyResource.RESOURCE; }
// This static inner class represents a heavy resource. privatestaticclassHeavyResource { HeavyResource() { // simulate expensive initialization... need a lot of time to load try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { thrownewRuntimeException(e); } }
voiduseResource() { System.out.println("Use this heavy resource..."); } } }
// Eager initialization: the single instance `currentRuntime` is initialized when the class is loaded, ensuring that the instance is created early (eagerly) and is thread-safe. publicclassRuntime { // holds the singleton instance: initialized when loading the class privatestaticRuntimecurrentRuntime=newRuntime(); // private constructor: prevents external instantiation privateRuntime() {}
// controls access to the singleton instance publicstatic Runtime getRuntime() { return currentRuntime; } // ... }
// controls access to the singleton instance: use static synchronized method to ensure thread safety publicstaticsynchronized SchedulerRepository getInstance() { if (inst == null) { inst = newSchedulerRepository(); } return inst; } // ... }
5.3. Spring - org.springframework.beans.factory.support.DefaultSingletonBeanRegistry (Lazy Singleton - Double-checked Locking)
Spring’s singleton pattern: When a bean is defined in the Spring application context without explicitly specified scope, Spring will treat it as a singleton by default. This means that Spring will create only one instance of the bean that can be shared by all the classes. By leveraging this kind of singleton pattern in Spring, we can benefit from efficient memory management, centralized bean configuration, and the sharing and reuse of bean instances across the application.
In Spring, the process of loading a singleton typically starts from the getBean() method of the org.springframework.beans.factory.BeanFactory, whose default implementation is the abstract class org.springframework.beans.factory.support.AbstractBeanFactory. The various getBean() methods ultimately call the doGetBean() method of AbstractBeanFactory, such as org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean.
// org.springframework.beans.factory.support.AbstractBeanFactory publicabstractclassAbstractBeanFactoryextendsFactoryBeanRegistrySupportimplementsConfigurableBeanFactory { // ... protected <T> T doGetBean(final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)throws BeansException { // get the transformed bean name, in case the name contains illegal characters finalStringbeanName= transformedBeanName(name); Object bean; // Eagerly check singleton cache for manually registered singletons. // In general, it will return null the first time a bean is requested. ObjectsharedInstance= getSingleton(beanName); if (sharedInstance != null && args == null) { // ... // get the object for the given bean instance, either the bean instance itself or its created object in case of a FactoryBean. bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); } else { // ... try { finalRootBeanDefinitionmbd= getMergedLocalBeanDefinition(beanName); // ... // Create bean instance. if (mbd.isSingleton()) { // If the bean is a singleton, it will enter the overloaded `getSingleton()` method. sharedInstance = getSingleton(beanName, newObjectFactory<Object>() { @Override public Object getObject()throws BeansException { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { destroySingleton(beanName); throw ex; } } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); } elseif (mbd.isPrototype()) { // ... } else { // ... } } catch (BeansException ex) { cleanupAfterBeanCreationFailure(beanName); throw ex; } } // ... return (T) bean; }
// ... }
The statement Object sharedInstance = getSingleton(beanName); within the getBean() method is the core implementation of the Singleton pattern (Double Check) in Spring. The getSingleton() method is defined in the parent class of AbstractBeanFactory, which is DefaultSingletonBeanRegistry.
// Cache of singleton objects (fully initialized singleton beans): bean name -> bean instance privatefinal Map<String, Object> singletonObjects = newConcurrentHashMap<>(256);
// Cache of early singleton objects (currently being created singleton beans, partially created): bean name -> bean instance // It's the key to resolving circular dependencies. privatefinal Map<String, Object> earlySingletonObjects = newHashMap<>(16);
// Cache of singleton factories: bean name -> ObjectFactory privatefinal Map<String, ObjectFactory<?>> singletonFactories = newHashMap<>(16);
@Override public Object getSingleton(String beanName) { return getSingleton(beanName, true); }
// get the bean from cache based on DCL pattern // `singletonObjects` -> `earlySingletonObjects` -> `singletonFactories` -> create the singleon bean through `singletonFactory.getObject()` protected Object getSingleton(String beanName, boolean allowEarlyReference) { // (1) `singletonObjects`: check if the cache contains the bean ObjectsingletonObject=this.singletonObjects.get(beanName); // The singleton in `singletonObjects` is not found. && The singleton bean is currently being created. // isSingletonCurrentlyInCreation(beanName): If it's true, when the singleton bean is in the process of being created but not yet fully initialized. if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { // synchronized on the `singletonObjects` map to ensure thread safety // (2) `earlySingletonObjects`: this cache holds beans that are in the process of being created. singletonObject = this.earlySingletonObjects.get(beanName); // The singleton in `earlySingletonObjects` is not found. && `allowEarlyReference` is true. if (singletonObject == null && allowEarlyReference) { // When certain methods require early initialization, the `addSingleFactory()` method is called to store the corresponding `ObjectFactory` initialization strategy in the `singletonFactories` map. // (3) `singletonFactories`: get the ObjectFactory by the beanName ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // create and get the bean singletonObject = singletonFactory.getObject(); // update the two caches: `earlySingletonObjects` and `singletonFactories` are mutually exclusive. this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } return (singletonObject != NULL_OBJECT ? singletonObject : null); }
// ... }
In general, the first time getSingleton() is called in doGetBean(), the corresponding bean instance is not found in the cache. It then proceeds to the if (mbd.isSingleton()) { } block, where the overloaded getSingleton() method is called. Within this method, the bean is instantiated, and the instance is then cached in the singletonObjects map, allowing for the direct return of the singleton bean in subsequent requests.
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(beanName, "'beanName' must not be null"); // Since the creation process involves operations on the `singletonObjects`, locking is required. synchronized (this.singletonObjects) { // 1. try to get the bean from `singletonObjects` map again and check if the bean has already been loaded. If the bean has been loaded, it is directly returned. ObjectsingletonObject=this.singletonObjects.get(beanName); if (singletonObject == null) { // 2. If the current BeanFactory is in the process of being destroyed, an exception is thrown directly, and the creation of the singleton bean is not allowed. if (this.singletonsCurrentlyInDestruction) { thrownewBeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction " + "(Do not request a bean from a BeanFactory in a destroy method implementation!)"); } if (logger.isDebugEnabled()) { logger.debug("Creating shared instance of singleton bean '" + beanName + "'"); } // 3. Some preparatory work before creating the bean: // The beanName is recorded as currently in the process of being loaded (added to the `singletonsCurrentlyInCreation` cache). // If the bean is already in the process of being loaded, an exception is thrown. // It's to address the issue of circular references. beforeSingletonCreation(beanName); booleannewSingleton=false; booleanrecordSuppressedExceptions= (this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = newLinkedHashSet<Exception>(); } try { // 4. Get the bean instance through a callback mechanism. singletonObject = singletonFactory.getObject(); newSingleton = true; } catch (IllegalStateException ex) { // Has the singleton object implicitly appeared in the meantime -> // if yes, proceed with it since the exception indicates that state. singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { throw ex; } } catch (BeanCreationException ex) { if (recordSuppressedExceptions) { for (Exception suppressedException : this.suppressedExceptions) { ex.addRelatedCause(suppressedException); } } throw ex; } finally { if (recordSuppressedExceptions) { this.suppressedExceptions = null; } // 5. Post-processing steps after the singleton bean is loaded: // The record of the bean being currently in the process of creation is removed (the beanName is removed from the `singletonsCurrentlyInCreation` cache). afterSingletonCreation(beanName); // 从 singletonsCurrentlyInCreation 中移除该 beanName } if (newSingleton) { // 6. added to the `singletonObjects` cache and removed from other auxiliary caches (e.g. `singletonFactories`, `earlySingletonObjects`) addSingleton(beanName, singletonObject); } } return (singletonObject != NULL_OBJECT ? singletonObject : null); } }
// ... }
5.4. Spring - org.springframework.core.ReactiveAdapterRegistry (Lazy Singleton - Double-checked Locking)