Singleton Pattern
2024-09-18  / 23 Design Patterns  / 1. Creational Pattern

0. Overview

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:

Type Thread Safety Loading Type Recommendable Reflection Attack Serialization Attack Perfect Version
1.1. Lazy Singleton - Thread Unsafe × Lazy - - -
1.2. Lazy Singleton - synchronized √ (synchronized static method) Lazy - - -
1.3. Lazy Singleton - Delay Lock × Lazy - - -
1.4. Lazy Singleton - Double-checked Locking Without volatile × Lazy - - -
1.5. Lazy Singleton - Double-checked Locking (DCL) √ (volatile instance + DCL) Lazy (DCL) 👍 × (1.5.1. Reflection Attack on Lazy Singleton - Double-checked Locking Singleton) √ (1.5.2. Serialization Attack on Lazy Singleton - Double-checked Locking Singleton) 1.5.3. Perfect Version of Lazy Singleton - Double-checked Locking Singleton
2.1. Eager Singleton - static constant √ (class loading mechanism) Eager (when class is loaded) 👍 √ (2.1.1. Reflection Attack on Eager Singleton - static constant) √ (2.1.2. Serialization Attack on Eager Singleton - static constant) 2.1.3. Perfect Version of Eager Singleton - static constant
2.2. Eager Singleton - static block √ (class loading mechanism) Eager (when class is loaded) 👍 √ (2.2.1. Reflection Attack on Eager Singleton - static block) √ (2.2.2. Serialization Attack on Eager Singleton - static block) 2.2.3. Perfect Version of Eager Singleton - static block
3. Static Inner Class Singleton √ (class loading mechanism) Lazy (static inner class) 👍 √ (3.1. Reflection Attack on Static Inner Class Singleton) √ (3.2. Serialization Attack on Static Inner Class Singleton) 3.3. Perfect Version of Static Inner Class Singleton
4. Enum Singleton √ (inherently safe, class loading mechanism) Eager (when the enum class is loaded) 👍 √ (4.1. Reflection Attack on Enum Singleton) √ (4.2. Serialization Attack on Enum Singleton) 4.3. Perfect Version of Enum Singleton

1. Lazy Singleton

Lazy initialization means that the instance can be created only when it is first accessed or the static method is first invoked.

1.1. Lazy Singleton - Thread Unsafe

It works fine in a single-threaded environment but may break the singleton pattern in a multi-threaded environment, resulting in different instances.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LazySingletonThreadUnsafe {

// holds the singleton instance
private static LazySingletonThreadUnsafe instance;

// private constructor: restricts users from creating instances themselves.
private LazySingletonThreadUnsafe() {
}

// controls access to the singleton instance
public static 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 = new LazySingletonThreadUnsafe();
}
return instance;
}
}

Testing Codes:

  • Single-threaded Environment: can get the same instance.
1
2
3
4
5
6
7
8
9
@Test
public void testLazySingletonThreadUnsafeSingleThread() {
LazySingletonThreadUnsafe instance1 = LazySingletonThreadUnsafe.getInstance();
LazySingletonThreadUnsafe instance2 = 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testLazySingletonThreadUnsafeMultiThread() throws ExecutionException, InterruptedException {
final int THREAD_COUNT = 100;
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

Callable<LazySingletonThreadUnsafe> callableTask = () -> LazySingletonThreadUnsafe.getInstance();
Future<LazySingletonThreadUnsafe>[] futures = new Future[THREAD_COUNT];

for (int i = 0; i < THREAD_COUNT; i++) {
futures[i] = executorService.submit(callableTask);
}

LazySingletonThreadUnsafe firstInstance = futures[0].get();
for (Future<LazySingletonThreadUnsafe> future : futures) {
LazySingletonThreadUnsafe instance = 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class LazySingletonSynchronized {

// holds the singleton instance
private static LazySingletonSynchronized instance;

// private constructor: restricts users from creating instances themselves.
private LazySingletonSynchronized() {
}

// synchronized static method: the class-level lock ensures that only one thread can execute the static synchronized method at a time.
public synchronized static 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 = new LazySingletonSynchronized();
}
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.

  1. T1: just enters the critical section and hasn’t instantiated yet: instance=null, T1 is holding the class-level lock
  2. 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
  3. 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
  4. T2: enters the critical section and executes instantiation again: instance=LazySingletonDelayLock@2db7a79b (Now it returns 2 different instances and breaks the singleton pattern!)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class LazySingletonDelayLock {

// holds the singleton instance
private static LazySingletonDelayLock instance;

// private constructor: restricts users from creating instances themselves.
private LazySingletonDelayLock() {
}

// delay lock in the if code block when `instance` is null
public static 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 = new LazySingletonDelayLock();
}
}
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 allocation2. initialization3. reference assignment.

The JVM allows for reordering of instructions for performance reasons, while these three steps may be reordered, such as 1. space allocation3. reference assignment2. initialization,  it can potentially lead to thread safety issues:

  1. T1: enters the critical section and ready to new
  2. T1: space allocation
  3. 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.)
  4. 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class LazySingletonNoVolatileDoubleCheck {

// holds the singleton instance
private static LazySingletonNoVolatileDoubleCheck instance;

// private constructor: restricts users from creating instances themselves.
private LazySingletonNoVolatileDoubleCheck() {
}

// add double-checked locking without volatile `instance`
public static 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 = new LazySingletonNoVolatileDoubleCheck();
// 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:

1
2
3
4
5
public class NewTest {
public static void main(String[] args) {
NewTest newTest = new NewTest();
}
}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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"
public class com.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
0 5 0 this Lcom/sissilab/dp/ox1_creational/ox11_singleton/NewTest;

public static void main(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
0 9 0 args [Ljava/lang/String;
8 1 1 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:

HeapStack0: new #2The `new` instruction opens a space in the heap and a (ref) reference returned will be pushed onto the top of the stack.refHeapStackref3: dupThe `dup` instruction duplicates the top-of-stack referenceand pushes onto the top of the stackrefHeapStackref4: invokespecial #3The `invokespecial` instruction pops the top-of-stack reference and the just-allocated space will be initialized through invoking constructorHeapStackref7: astore_1The top-of-stack reference is popped and assigned to the element (`newTest`) at the position 1 of the LocalVariableTableLocalVariableTable0 args1 newTest

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.

1.5. Lazy Singleton - Double-checked Locking (DCL)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 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.
*/
class LazySingletonDoubleCheck {

// holds the singleton instance: add volatile to prevent reordering
private static volatile LazySingletonDoubleCheck instance;

// private constructor: restricts users from creating instances themselves.
private LazySingletonDoubleCheck() {
}

// use double-checked locking
public static LazySingletonDoubleCheck getInstance() {
if (null == instance) {
synchronized (LazySingletonDoubleCheck.class) {
if (null == instance) {
instance = new LazySingletonDoubleCheck();
}
}
}
return 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:

  1. 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.
  2. It lacks any inherent mechanism to check if the instance of a singleton class already exists when accessed via reflection.
  3. 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class LazySingletonDoubleCheckReflectionProof {

// holds the singleton instance: add volatile to prevent reordering
private static volatile LazySingletonDoubleCheckReflectionProof instance;

// private constructor: restricts users from creating instances themselves, but cannot prevent constructor reflection
private LazySingletonDoubleCheckReflectionProof() {
// cannot prevent reflection attack: the first call using reflection will still succeed, but subsequent attempts will fail.
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// use double-checked locking
public static LazySingletonDoubleCheckReflectionProof getInstance() {
if (null == instance) {
synchronized (LazySingletonDoubleCheckReflectionProof.class) {
if (null == instance) {
instance = new LazySingletonDoubleCheckReflectionProof();
}
}
}
return instance;
}
}

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:

  1. Serializing the singleton class to generate a serialized file.
  2. Reading the serialized file to obtain the instance of the singleton class via deserialization.
  3. Comparing whether the serialized instance and the deserialized instance are the same.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// The class has to implement `java.io.Serializable` to avoid throwing `java.io.NotSerializableException`.
class LazySingletonDoubleCheckSerialization implements Serializable {

// holds the singleton instance: add volatile to prevent reordering
private static volatile LazySingletonDoubleCheckSerialization instance;

// private constructor: restricts users from creating instances themselves.
private LazySingletonDoubleCheckSerialization() {
// cannot prevent reflection attacks in all cases
iif (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// use double-checked locking
public static LazySingletonDoubleCheckSerialization getInstance() {
if (null == instance) {
synchronized (LazySingletonDoubleCheckSerialization.class) {
if (null == instance) {
instance = new LazySingletonDoubleCheckSerialization();
}
}
}
return 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void testSerializationAttackOnLazySingletonDoubleCheckSerialization() {
LazySingletonDoubleCheckSerialization instance = LazySingletonDoubleCheckSerialization.getInstance();

final Path serializedClassPath = Paths.get("TestSerialization-" + LazySingletonDoubleCheckSerialization.class.getSimpleName());

// serialize:
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(serializedClassPath))) {
objectOutputStream.writeObject(instance);
} catch (IOException e) {
throw new RuntimeException(e);
}

// deserialize: the constructor will not be called, the data will be initialized by reading from the byte stream.
LazySingletonDoubleCheckSerialization testSerializableInstance;
try (ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(serializedClassPath))) {
testSerializableInstance = ((LazySingletonDoubleCheckSerialization) objectInputStream.readObject());
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}

Assertions.assertSame(instance, testSerializableInstance); // ×
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class LazySingletonDoubleCheckSerializationProof implements Serializable {

/**
* 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.
*/
private static final long serialVersionUID = -1L;

// holds the singleton instance: add volatile to prevent reordering
private static volatile LazySingletonDoubleCheckSerializationProof instance;

// private constructor: restricts users from creating instances themselves.
private LazySingletonDoubleCheckSerializationProof() {
// cannot prevent reflection attacks in all cases
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// use double-checked locking
public static LazySingletonDoubleCheckSerializationProof getInstance() {
if (null == instance) {
synchronized (LazySingletonDoubleCheckSerializationProof.class) {
if (null == instance) {
instance = new LazySingletonDoubleCheckSerializationProof();
}
}
}
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private Object readOrdinaryObject(boolean unshared) throws IOException {
// ...

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.
Object rep = 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;
}

boolean hasReadResolveMethod() {
requireInitialized();
// if the `readResolveMethod` is not null, it means the current class has the `readResolve()` method.
return (readResolveMethod != null);
}

private ObjectStreamClass(final Class<?> cl) {
// ...

// get the `readResolve()` method of the current class and assign it to the `readResolveMethod` variable
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);

// ...
}

1.5.3. Perfect Version of ‘Lazy Singleton - DCL’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 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
*/
public final class LazySingletonDoubleCheckPerfect implements Serializable {

// Explicitly defining `serialVersionUID` can avoid potential incompatibility issues.
private static final long serialVersionUID = -1L;

// holds the singleton instance: add volatile to prevent reordering
private static volatile LazySingletonDoubleCheckPerfect instance;

// private constructor: restricts users from creating instances themselves.
private LazySingletonDoubleCheckPerfect() {
// cannot prevent reflection attacks in all cases
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// use double-checked locking
public static LazySingletonDoubleCheckPerfect getInstance() {
if (null == instance) {
synchronized (LazySingletonDoubleCheckPerfect.class) {
if (null == instance) {
instance = new LazySingletonDoubleCheckPerfect();
}
}
}
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
throw new CloneNotSupportedException("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:

  1. Loading: the JVM locates and loads the class file’s bytecode into memory, generating the corresponding Class data structure.
  2. 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)
  3. 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.
  4. Using: After a class is loaded and initialized, its instances can be created, methods can be invoked, and fields can be accessed.
  5. 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.

Please refer to this example EagerSingletonWastingReason.java at sissilab/DesignPattern-Lab (github.com) for more details.

2.1. Eager Singleton - static constant

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 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.
*/
class EagerSingletonStaticConstant {

// holds the singleton instance: initialized when loading the class
private static final EagerSingletonStaticConstant instance = new EagerSingletonStaticConstant();

// private constructor: restricts users from creating instances themselves.
private EagerSingletonStaticConstant() {
}

// controls access to the singleton instance
public static EagerSingletonStaticConstant getInstance() {
return instance;
}
}

2.1.1. Reflection Attack on Eager Singleton - static constant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class EagerSingletonStaticConstantReflectionProof {

// holds the singleton instance: initialized when loading the class
private static final EagerSingletonStaticConstantReflectionProof instance = new EagerSingletonStaticConstantReflectionProof();

// private constructor: restricts users from creating instances themselves
private EagerSingletonStaticConstantReflectionProof() {
// prevent reflection attack
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static EagerSingletonStaticConstantReflectionProof getInstance() {
return instance;
}
}

2.1.2. Serialization Attack on Eager Singleton - static constant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class EagerSingletonStaticConstantSerializationProof implements Serializable {

// Explicitly defining `serialVersionUID` can avoid potential incompatibility issues.
private static final long serialVersionUID = -1L;

// holds the singleton instance: initialized when loading the class
private static final EagerSingletonStaticConstantSerializationProof instance = new EagerSingletonStaticConstantSerializationProof();

// private constructor: restricts users from creating instances themselves
private EagerSingletonStaticConstantSerializationProof() {
// prevent reflection attack
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 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
*/
public final class EagerSingletonStaticConstantPerfect implements Serializable {

// Explicitly defining `serialVersionUID` can avoid potential incompatibility issues.
private static final long serialVersionUID = -1L;

// holds the singleton instance: initialized when loading the class
private static final EagerSingletonStaticConstantPerfect instance = new EagerSingletonStaticConstantPerfect();

// private constructor: restricts users from creating instances themselves.
private EagerSingletonStaticConstantPerfect() {
// cannot prevent reflection attacks in all cases
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static 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
throw new CloneNotSupportedException("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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 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.
*/
class EagerSingletonStaticBlock {

// holds the singleton instance: initialized when loading the class
private static final EagerSingletonStaticBlock instance;

static {
// ...
instance = new EagerSingletonStaticBlock();
// ...
}

// private constructor: restricts users from creating instances themselves.
private EagerSingletonStaticBlock() {
}

// controls access to the singleton instance
public static EagerSingletonStaticBlock getInstance() {
return instance;
}
}

2.2.1. Reflection Attack on Eager Singleton - static block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class EagerSingletonStaticBlockReflectionProof {

// holds the singleton instance: initialized when loading the class in the static block
private static final EagerSingletonStaticBlockReflectionProof instance;

static {
instance = new EagerSingletonStaticBlockReflectionProof();
}

// private constructor: restricts users from creating instances themselves
private EagerSingletonStaticBlockReflectionProof() {
// prevent reflection attack
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static EagerSingletonStaticBlockReflectionProof getInstance() {
return instance;
}
}

2.2.2. Serialization Attack on Eager Singleton - static block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class EagerSingletonStaticBlockSerializationProof implements Serializable {

// Explicitly defining `serialVersionUID` can avoid potential incompatibility issues.
private static final long serialVersionUID = -1L;

// holds the singleton instance: initialized when loading the class in the static block
private static final EagerSingletonStaticBlockSerializationProof instance;

static {
instance = new EagerSingletonStaticBlockSerializationProof();
}

// private constructor: restricts users from creating instances themselves
private EagerSingletonStaticBlockSerializationProof() {
// prevent reflection attack
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 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
*/
public final class EagerSingletonStaticBlockPerfect implements Serializable {

// Explicitly defining `serialVersionUID` can avoid potential incompatibility issues.
private static final long serialVersionUID = -1L;

// holds the singleton instance: initialized when loading the class
private static final EagerSingletonStaticBlockPerfect instance;

static {
// ...
instance = new EagerSingletonStaticBlockPerfect();
// ...
}

// private constructor: restricts users from creating instances themselves.
private EagerSingletonStaticBlockPerfect() {
// cannot prevent reflection attacks in all cases
if (null != instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static 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
throw new CloneNotSupportedException("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 1.5. Lazy Singleton - Double-checked Locking (DCL), this pattern can also achieve the similar effect of lazy loading but with a simpler implementation.
  • 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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 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
*/
public class StaticInnerClassSingleton {

// static inner class to provide the singleton instance of the class
private static class InstanceHolder {
private static StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
}

// private constructor: restricts users from creating instances themselves.
private StaticInnerClassSingleton() {
}

// controls access to the singleton instance
public static 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class StaticInnerSingletonReflectionProof {

// static inner class to provide the singleton instance of the class
private static class InstanceHolder {
private static StaticInnerSingletonReflectionProof instance = new StaticInnerSingletonReflectionProof();
}

// private constructor: restricts users from creating instances themselves, but cannot prevent constructor reflection
private StaticInnerSingletonReflectionProof() {
// prevent reflection attack
if (null != InstanceHolder.instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class StaticInnerSingletonSerializationProof implements Serializable {

/**
* 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.
*/
private static final long serialVersionUID = -1L;

// static inner class to provide the singleton instance of the class
private static class InstanceHolder {
private static StaticInnerSingletonSerializationProof instance = new StaticInnerSingletonSerializationProof();
}

// private constructor: restricts users from creating instances themselves, but cannot prevent constructor reflection
private StaticInnerSingletonSerializationProof() {
// prevent reflection attack
if (null != InstanceHolder.instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 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
*/
public final class StaticInnerSingletonPerfect implements Serializable {

// Explicitly defining `serialVersionUID` can avoid potential incompatibility issues.
private static final long serialVersionUID = -1L;

// static inner class to provide the singleton instance of the class
private static class InstanceHolder {
private static StaticInnerSingletonPerfect instance = new StaticInnerSingletonPerfect();
}

// private constructor: restricts users from creating instances themselves
private StaticInnerSingletonPerfect() {
// prevent reflection attack
if (null != InstanceHolder.instance) {
throw new IllegalStateException("This singleton instance already exists.");
}
}

// controls access to the singleton instance
public static 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
throw new CloneNotSupportedException("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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 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, ...
*/
public enum EnumSingleton {
instance;

public void anyMethod() {
}
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
Classfile /DesignPattern-Lab/src/main/java/com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton.class
Last modified 26/08/2024; size 1195 bytes
MD5 checksum 1e21098493b1502d48f3a4fba5884dcc
Compiled from "EnumSingleton.java"
// enum class implicitly exted `java.lang.Enum`
public final class com.sissilab.dp.ox1_creational.ox11_singleton.EnumSingleton extends java.lang.Enum<com.sissilab.dp.ox1_creational.ox11_singleton.EnumSingleton>
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:
#1 = Fieldref #4.#34 // com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton.$VALUES:[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
#2 = Methodref #35.#36 // "[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;".clone:()Ljava/lang/Object;
#3 = Class #14 // "[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;"
#4 = Class #37 // com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton
#5 = Methodref #10.#38 // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
#6 = Methodref #10.#39 // java/lang/Enum."<init>":(Ljava/lang/String;I)V
#7 = String #11 // instance
#8 = Methodref #4.#39 // com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton."<init>":(Ljava/lang/String;I)V
#9 = Fieldref #4.#40 // com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton.instance:Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
#10 = Class #41 // java/lang/Enum
#11 = Utf8 instance
#12 = Utf8 Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
#13 = Utf8 $VALUES
#14 = Utf8 [Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
#15 = Utf8 values
#16 = Utf8 ()[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 valueOf
#20 = Utf8 (Ljava/lang/String;)Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
#21 = Utf8 LocalVariableTable
#22 = Utf8 name
#23 = Utf8 Ljava/lang/String;
#24 = Utf8 <init>
#25 = Utf8 (Ljava/lang/String;I)V
#26 = Utf8 this
#27 = Utf8 Signature
#28 = Utf8 ()V
#29 = Utf8 anyMethod
#30 = Utf8 <clinit>
#31 = Utf8 Ljava/lang/Enum<Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;>;
#32 = Utf8 SourceFile
#33 = Utf8 EnumSingleton.java
#34 = NameAndType #13:#14 // $VALUES:[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
#35 = Class #14 // "[Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;"
#36 = NameAndType #42:#43 // clone:()Ljava/lang/Object;
#37 = Utf8 com/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton
#38 = NameAndType #19:#44 // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
#39 = NameAndType #24:#25 // "<init>":(Ljava/lang/String;I)V
#40 = NameAndType #11:#12 // instance:Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
#41 = Utf8 java/lang/Enum
#42 = Utf8 clone
#43 = Utf8 ()Ljava/lang/Object;
#44 = Utf8 (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
{
// The `instance` is a `public static final` field.
public static final com.sissilab.dp.ox1_creational.ox11_singleton.EnumSingleton instance;
descriptor: Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

// the `private static final $VALUES` field holds all enum constants
private static final 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.
public static 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

public static 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
0 10 0 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
0 7 0 this Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;
Signature: #28 // ()V

public void anyMethod();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 21: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/sissilab/dp/ox1_creational/ox11_singleton/EnumSingleton;

// 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

// ldc: loads String instance onto the stack
4: ldc #7 // String instance

// 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
public void testReflectionAttackOnEnumSingleton() 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
EnumSingleton reflectInstance = constructor.newInstance("instance", 0); // java.lang.IllegalArgumentException: Cannot reflectively create enum objects

// instance: get the instance through the normal public static final `instance` field
EnumSingleton instance = EnumSingleton.instance;

Assertions.assertSame(reflectInstance, 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final class Constructor<T> extends Executable {
// ...

@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)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private Enum<?> readEnum(boolean unshared) throws IOException {
// only objects serialized as enum are processed
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}

ObjectStreamClass desc = readClassDesc(false);
// If the class isn't an enum, an InvalidClassException will be thrown.
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}

int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}

// get the name of the enum constant, e.g. `instance` (Each constant name must be unique within an enum.)
String name = 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) new InvalidObjectException(
"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);
}
}

handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}

public static <T extends Enum<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.
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}

4.3. Perfect Version of Enum Singleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Use this singleton:
* `EnumSingletonPerfect.instance.anyMethod();`
*/
public enum EnumSingletonPerfect {
instance;

public void anyMethod() {
}

/**
* Access the lazy-loaded resource:
* `EnumSingletonPerfect.instance.getExpensiveResource().useResource();`
*/
// Using a static inner class to implement lazy loading of heavy resources
private static class LazyHeavyResource {
private static final HeavyResource RESOURCE = new HeavyResource();
}

public HeavyResource getExpensiveResource() {
return LazyHeavyResource.RESOURCE;
}

// This static inner class represents a heavy resource.
private static class HeavyResource {
HeavyResource() {
// simulate expensive initialization... need a lot of time to load
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

void useResource() {
System.out.println("Use this heavy resource...");
}
}
}

5. Application in Source Code

5.1. java.lang.Runtime (Eager Singleton - static constant)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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.
public class Runtime {
// holds the singleton instance: initialized when loading the class
private static Runtime currentRuntime = new Runtime();

// private constructor: prevents external instantiation
private Runtime() {}

// controls access to the singleton instance
public static Runtime getRuntime() {
return currentRuntime;
}

// ...
}

5.2. org.quartz.impl.SchedulerRepository (Lazy Singleton - synchronized)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Scheduler scheduler = new StdSchedulerFactory().getScheduler(); -> SchedulerRepository schedRep = SchedulerRepository.getInstance();
public class SchedulerRepository {
// holds the singleton instance
private static SchedulerRepository inst;

// private constructor: prevents external instantiation
private SchedulerRepository() {}

// controls access to the singleton instance: use static synchronized method to ensure thread safety
public static synchronized SchedulerRepository getInstance() {
if (inst == null) {
inst = new SchedulerRepository();
}
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// org.springframework.beans.factory.support.AbstractBeanFactory
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
// ...

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
final String beanName = 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.
Object sharedInstance = 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 {
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// ...

// Create bean instance.
if (mbd.isSingleton()) {
// If the bean is a singleton, it will enter the overloaded `getSingleton()` method.
sharedInstance = getSingleton(beanName, new ObjectFactory<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);
} else if (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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// ...

// Cache of singleton objects (fully initialized singleton beans): bean name -> bean instance
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(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.
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// Cache of singleton factories: bean name -> ObjectFactory
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(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
Object singletonObject = 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// ...

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.
Object singletonObject = 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) {
throw new BeanCreationNotAllowedException(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);
boolean newSingleton = false;
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReactiveAdapterRegistry {
// holds the singleton instance: add volatile to prevent reordering
@Nullable
private static volatile ReactiveAdapterRegistry sharedInstance;

// use double-checked locking
public static ReactiveAdapterRegistry getSharedInstance() {
ReactiveAdapterRegistry registry = sharedInstance;
if (registry == null) {
synchronized (ReactiveAdapterRegistry.class) {
registry = sharedInstance;
if (registry == null) {
registry = new ReactiveAdapterRegistry();
sharedInstance = registry;
}
}
}
return registry;
}

// ...
}

5.5. Spring - org.springframework.aop.framework.ProxyFactoryBean (Lazy Singleton - synchronized)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ProxyFactoryBean extends ProxyCreatorSupport
implements FactoryBean<Object>, BeanClassLoaderAware, BeanFactoryAware {

private Object singletonInstance;

// controls access to the singleton instance: use synchronized method to ensure thread safety
private synchronized Object getSingletonInstance() {
if (this.singletonInstance == null) {
this.targetSource = freshTargetSource();
if (this.autodetectInterfaces && getProxiedInterfaces().length == 0 && !isProxyTargetClass()) {
// Rely on AOP infrastructure to tell us what interfaces to proxy.
Class<?> targetClass = getTargetClass();
if (targetClass == null) {
throw new FactoryBeanNotInitializedException("Cannot determine target class for proxy");
}
setInterfaces(ClassUtils.getAllInterfacesForClass(targetClass, this.proxyClassLoader));
}
// Initialize the shared singleton instance.
super.setFrozen(this.freezeProxy);
// create AOP dynamic proxy and get the instance
this.singletonInstance = getProxy(createAopProxy());
}
return this.singletonInstance;
}

// ...
}

5.6. Tomcat - org.apache.catalina.webresources.TomcatURLStreamHandlerFactory (Lazy Singleton - Double-checked Locking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class TomcatURLStreamHandlerFactory implements URLStreamHandlerFactory {

// holds the singleton instance: add volatile to prevent reordering
private static volatile TomcatURLStreamHandlerFactory instance = null;

public static TomcatURLStreamHandlerFactory getInstance() {
getInstanceInternal(true);
return instance;
}

// use double-checked locking
private static TomcatURLStreamHandlerFactory getInstanceInternal(boolean register) {
if (instance == null) {
Class var1 = TomcatURLStreamHandlerFactory.class;
synchronized(TomcatURLStreamHandlerFactory.class) {
if (instance == null) {
instance = new TomcatURLStreamHandlerFactory(register);
}
}
}

return instance;
}

// ...
}

References