Singleton Pattern
What is it? (The Analogy)
Think about the principal of a school. There's only ONE principal at a time. You can't just create a new principal whenever you feel like it. Everyone in the school -- teachers, students, parents -- all interact with the same principal. If the math teacher and the gym teacher both go to "the principal," they're talking to the same person.
Now imagine if every teacher could create their own personal principal. Teacher A's principal says "No homework on Fridays," Teacher B's principal says "Double homework on Fridays." Total confusion! That's why you need exactly ONE principal -- a single, shared instance that everyone references.
The Singleton Pattern ensures a class has only one instance in the entire application and provides a global access point to that instance. It's like having one principal's office with one door that everyone uses.
Why do we need it?
Some things in a program MUST be unique. If you have two database connection pools, they might exhaust the database. If you have two configuration managers, they might give conflicting settings. If you have two loggers writing to the same file, the output gets garbled.
1// BAD EXAMPLE -- Nothing prevents multiple instances!
2public class GameSettings {
3 private int volume;
4 private int brightness;
5 private String difficulty;
6
7 public GameSettings() {
8 // Load default settings
9 this.volume = 70;
10 this.brightness = 50;
11 this.difficulty = "Normal";
12 }
13
14 public void setVolume(int volume) { this.volume = volume; }
15 public int getVolume() { return volume; }
16 // ... more getters and setters
17}
18
19// Problem: Different parts of the game create their own settings!
20public class MainMenu {
21 GameSettings settings = new GameSettings(); // Instance 1
22 // User sets volume to 30 here
23}
24
25public class GameScreen {
26 GameSettings settings = new GameSettings(); // Instance 2 -- DIFFERENT object!
27 // This has volume 70 (default), not 30!
28 // The user's setting is LOST!
29}The pain: Multiple instances of something that should be unique causes data inconsistency. Settings changed in one place don't appear in another. Resources get duplicated. Things fall out of sync.
How it works -- Step by Step
- Make the constructor private -- No one outside the class can call
new.
- Create a private static variable to hold the ONE instance.
- Create a public static method (usually called
getInstance()) that returns the instance. If it doesn't exist yet, create it. If it already exists, return the existing one.
- Handle thread safety (in multi-threaded applications) -- Make sure two threads can't accidentally create two instances at the same time.
- Use the singleton by calling
ClassName.getInstance()everywhere instead ofnew ClassName().
Let's Build It Together
Let's build a High Score Manager for a video game -- there must be exactly ONE high score list shared across all levels, menus, and screens.
1// ========================================
2// Step 1: The BASIC Singleton (not thread-safe yet)
3// ========================================
4
5public class HighScoreManager {
6 // Step 1a: Private static variable holds the ONE instance
7 private static HighScoreManager instance;
8
9 // The actual data this singleton manages
10 private List<Map.Entry<String, Integer>> highScores;
11 private static final int MAX_SCORES = 10;
12
13 // Step 1b: PRIVATE constructor -- nobody outside can call "new"
14 private HighScoreManager() {
15 System.out.println("HighScoreManager created! (This should print only ONCE)");
16 this.highScores = new ArrayList<>();
17 }
18
19 // Step 1c: Public static method to get the instance
20 public static HighScoreManager getInstance() {
21 if (instance == null) {
22 // First time? Create the instance.
23 instance = new HighScoreManager();
24 }
25 // Already exists? Return the same one.
26 return instance;
27 }
28
29 // Business methods -- the actual functionality
30 public void addScore(String playerName, int score) {
31 highScores.add(Map.entry(playerName, score));
32 // Sort by score descending
33 highScores.sort((a, b) -> b.getValue() - a.getValue());
34 // Keep only top scores
35 if (highScores.size() > MAX_SCORES) {
36 highScores = new ArrayList<>(highScores.subList(0, MAX_SCORES));
37 }
38 System.out.println("Score added: " + playerName + " - " + score);
39 }
40
41 public void displayHighScores() {
42 System.out.println("\n========== HIGH SCORES ==========");
43 for (int i = 0; i < highScores.size(); i++) {
44 var entry = highScores.get(i);
45 System.out.printf(" #%d %-15s %d%n", i + 1, entry.getKey(), entry.getValue());
46 }
47 System.out.println("==================================\n");
48 }
49
50 public int getTopScore() {
51 if (highScores.isEmpty()) return 0;
52 return highScores.get(0).getValue();
53 }
54}1// ========================================
2// Step 2: The THREAD-SAFE Singleton
3// Uses "double-checked locking" for performance
4// ========================================
5
6public class HighScoreManagerThreadSafe {
7 // "volatile" ensures all threads see the latest value
8 private static volatile HighScoreManagerThreadSafe instance;
9
10 private List<Map.Entry<String, Integer>> highScores;
11
12 private HighScoreManagerThreadSafe() {
13 this.highScores = new ArrayList<>();
14 }
15
16 public static HighScoreManagerThreadSafe getInstance() {
17 // First check (no locking -- fast!)
18 if (instance == null) {
19 // Only lock if instance might need to be created
20 synchronized (HighScoreManagerThreadSafe.class) {
21 // Second check (with lock -- safe!)
22 if (instance == null) {
23 instance = new HighScoreManagerThreadSafe();
24 }
25 }
26 }
27 return instance;
28 }
29
30 // ... same business methods as before
31}1// ========================================
2// Step 3: The SIMPLEST thread-safe approach (recommended!)
3// Uses Java's class loading guarantee
4// ========================================
5
6public class HighScoreManagerSimple {
7 // Java guarantees this is created exactly once, thread-safely
8 private static final HighScoreManagerSimple INSTANCE = new HighScoreManagerSimple();
9
10 private List<Map.Entry<String, Integer>> highScores;
11
12 private HighScoreManagerSimple() {
13 this.highScores = new ArrayList<>();
14 }
15
16 public static HighScoreManagerSimple getInstance() {
17 return INSTANCE; // Already created, just return it
18 }
19
20 // ... same business methods
21}1// ========================================
2// Step 4: Use it across the game!
3// ========================================
4
5// Simulates Level 1 of the game
6class Level1 {
7 public void onLevelComplete(String playerName, int score) {
8 // Gets THE SAME instance
9 HighScoreManager.getInstance().addScore(playerName, score);
10 }
11}
12
13// Simulates Level 2 of the game
14class Level2 {
15 public void onLevelComplete(String playerName, int score) {
16 // Gets THE SAME instance -- all scores are in one place!
17 HighScoreManager.getInstance().addScore(playerName, score);
18 }
19}
20
21// Simulates the main menu
22class MainMenu {
23 public void showHighScores() {
24 // Gets THE SAME instance -- sees ALL scores from ALL levels!
25 HighScoreManager.getInstance().displayHighScores();
26 }
27}
28
29// Run the game!
30public class GameApp {
31 public static void main(String[] args) {
32 // Different parts of the game add scores
33 Level1 level1 = new Level1();
34 level1.onLevelComplete("Alice", 5000);
35 level1.onLevelComplete("Bob", 3200);
36
37 Level2 level2 = new Level2();
38 level2.onLevelComplete("Alice", 7800);
39 level2.onLevelComplete("Charlie", 6100);
40
41 // Main menu sees ALL scores from everywhere
42 MainMenu menu = new MainMenu();
43 menu.showHighScores();
44
45 // Verify it's the same instance
46 System.out.println("Top score: " + HighScoreManager.getInstance().getTopScore());
47 }
48}Aha moment: Level1, Level2, and MainMenu all call HighScoreManager.getInstance() and they ALL get back the exact same object. Scores added in Level 1 are visible when Level 2 queries them. One instance, shared everywhere, consistent data.
Visual Mental Model
1 Level1 Level2 MainMenu OptionsScreen
2 | | | |
3 v v v v
4 +---------- getInstance() --------------------+
5 |
6 v
7 +-------------------+
8 | HighScoreManager | <-- Only ONE of these exists!
9 | |
10 | - highScores |
11 | - MAX_SCORES |
12 | |
13 | + addScore() |
14 | + displayScores() |
15 | + getTopScore() |
16 +-------------------+
17 (private constructor)
18
19 No matter who calls getInstance(), they ALL get the SAME object.Common Mistakes & Gotchas
- Overusing Singleton -- Not everything needs to be a singleton! Singleton should be used for things that are truly shared global state: config, logging, connection pools. If you're using it just for convenience, you're probably creating a hidden global variable.
- Thread safety in the basic version -- The simple
if (instance == null)check is NOT thread-safe. Two threads could both seenulland create two instances. Use one of the thread-safe approaches shown above. - Testing is harder -- Singletons carry state across tests. One test can pollute another. Consider adding a
resetForTesting()method or using dependency injection. - Forgetting to make the constructor private -- If you forget
private, anyone can still callnewand bypass the singleton. - Serialization breaks singletons -- If you serialize/deserialize a singleton, Java creates a new object. You need to implement
readResolve()to fix this. (Advanced topic.) - Enum Singletons -- The most bulletproof Java singleton is an
enumwith one constant. It handles serialization and thread safety automatically. Look it up once you're comfortable with the basics!
Interview Tip
Interviewers love to ask: "How do you make a Singleton thread-safe?" Know three approaches: (1) Eager initialization with static final, (2) Double-checked locking with volatile, (3) Enum singleton. Also be ready to discuss the downsides of Singleton -- it introduces global state, makes testing harder, and can hide dependencies. Showing you understand the trade-offs demonstrates maturity.
Quick Quiz
- Why must the constructor of a Singleton class be
private? What happens if you make itpublic?
- In a multi-threaded application, two threads call
getInstance()at the exact same time wheninstanceis stillnull. Walk through what happens step by step with the basic (non-thread-safe) version. What goes wrong?
- Your application has a
DatabaseConnectionPoolsingleton and aConfigManagersingleton. The connection pool needs settings from the config manager during initialization. What could go wrong? (Hint: think about initialization order.)
Summary -- Key Takeaways
- Singleton ensures exactly one instance of a class exists in the entire application, with a global access point via
getInstance(). - Private constructor + static instance + public static method is the classic recipe.
- Thread safety matters -- use eager initialization (
static final), double-checked locking (volatile+synchronized), or enum singletons. - Use sparingly -- Singletons are essentially global variables. They're great for configuration, logging, and connection pools, but overuse leads to tightly coupled, hard-to-test code.