Why the Singleton Pattern Feels Simple Until It Doesn’t
The Singleton pattern sounds harmless when a program has one flow of control. One object, one shared state, one obvious place to look. Threads remove that comfort quickly. The moment two running paths touch the same shared object at the same time, Singleton stops feeling like a neat structural idea and starts feeling like a coordination problem.
That is the only job of this article. Not to define Singleton again, and not to decide whether the pattern is broadly good or bad. This piece is about what happens when one-instance design meets concurrency, and why mutexes make the trade-offs impossible to ignore.
Shared State Looks Fine Until Two Threads Touch It
import threading
counter = 0
def increment():
global counter
for _ in range(100_000):
counter += 1
thread_a = threading.Thread(target=increment)
thread_b = threading.Thread(target=increment)
thread_a.start()
thread_b.start()
thread_a.join()
thread_b.join()
print(counter)
A beginner might expect the result to be exactly 200000 every time. The problem is
that both threads are touching the same data. The constraint is that they are doing it at the
same time, without coordination.
A Mutex Is the First Real Correction
import threading
counter = 0
counter_lock = threading.Lock()
def increment():
global counter
for _ in range(100_000):
with counter_lock:
counter += 1
A mutex is just a lock. It narrows access so one thread enters the critical section at a time. The context manager matters because the lock is released automatically when the block exits.
Why This Connects to Singleton
Singleton becomes more serious the moment one shared object starts owning one shared lock and one shared piece of state.
That is where the pattern starts to feel practical and dangerous at the same time. Practical, because one object can centralize responsibility. Dangerous, because the same shared object can also become the place where unrelated code collides.
A Singleton Counter Manager
import threading
class CounterManager:
instance = None
class __CounterManager:
def __init__(self):
self.value = 0
self.lock = threading.Lock()
def increment(self):
with self.lock:
self.value += 1
def __str__(self):
return f"CounterManager(value={self.value})"
def __new__(cls):
if cls.instance is None:
cls.instance = cls.__CounterManager()
return cls.instance
This object has one counter and one lock. Every caller who asks for CounterManager()
gets the same shared instance back.
Using It Across Threads
import threading
def worker():
manager = CounterManager()
for _ in range(100_000):
manager.increment()
thread_a = threading.Thread(target=worker)
thread_b = threading.Thread(target=worker)
thread_a.start()
thread_b.start()
thread_a.join()
thread_b.join()
manager = CounterManager()
print(manager)
This is the useful version of the pattern under concurrency. One object owns one shared responsibility, and that object also owns the lock needed to protect the state it exposes.
Why the Pattern Feels Riskier Here
Threads make the true cost of Singleton harder to ignore. One shared object means one shared place where behavior can ripple outward. If the design is sloppy, the same global reach that makes the object convenient also makes the consequences wider.
That is why Singleton becomes less romantic under concurrency. The object either owns one real responsibility clearly, or it becomes a vague shared container with too much reach.
What a Beginner Should Keep
Concurrency is one of the clearest ways to understand the real trade in Singleton. One instance can bring order when one object truly should coordinate one shared resource. But one instance also means shared state, and shared state means mistakes travel farther.
Further Reading
If you want the broader decision post next, read When the Singleton Pattern Actually Helps .
If you want implementation comparison next, read Comparing Two Singleton Implementations in Python .
If you want the Python behavior angle next, read How Singleton Works in Python Without Leaning on Strict Types .
Comments