| Lesson 11 | Variations on the Singleton Pattern |
| Objective | Write a Class that uses an Extension to the Singleton Pattern |
Singleton is a specific promise: exactly one instance within a defined boundary, plus a well-known access point. But the mechanics behind Singleton—controlled construction and controlled access—can be reused to build related solutions.
One common extension is to keep a controlled set of reusable objects and hand them out on demand. At that point, you are no longer implementing “Singleton” as the end goal—you are implementing an Object Pool (or a Multiton) using singleton-like access.
This distinction is important: the Singleton is the “owner” (the broker), while the pooled objects are the “resources.”
The Object Pool pattern maintains a set of initialized objects ready to use, instead of allocating and destroying them repeatedly. It is most valuable when object creation is expensive and reuse is safe (for example, reusable buffers, parser instances, or fixed-capacity resources).
In older designs you might see “pool of threads,” but in modern Java the correct abstraction is usually a pool of workers managed internally,
with clients submitting tasks. That is why the standard library provides ExecutorService and ThreadPoolExecutor.
If your real goal is “reuse execution capacity,” do not return raw Thread objects. Instead, expose a method that submits work. The Singleton aspect is simply the shared access point to a single executor within your application boundary.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public final class AppExecutor {
// Singleton instance (eager is simplest; lazy can also be done safely)
private static final AppExecutor INSTANCE = new AppExecutor();
private final ExecutorService pool;
private AppExecutor() {
// Fixed pool as a clear example; tune size for your workload.
this.pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
}
public static AppExecutor instance() {
return INSTANCE;
}
public Future<?> submit(Runnable task) {
return pool.submit(task);
}
public void shutdown() {
pool.shutdown();
}
}
This “Singleton executor” is a common real-world pattern: one broker manages the shared resource and enforces a consistent policy (pool sizing, naming, monitoring, shutdown).
If you want to demonstrate the “store created objects and reuse idle ones” idea directly, use a pool of lightweight objects.
The example below keeps a bounded pool of reusable StringBuilder instances (stand-in for any reusable resource).
import java.util.ArrayDeque;
import java.util.Deque;
public final class StringBuilderPool {
private static final StringBuilderPool INSTANCE = new StringBuilderPool();
private final Deque<StringBuilder> idle = new ArrayDeque<>();
private final int maxSize = 32;
private StringBuilderPool() { }
public static StringBuilderPool instance() {
return INSTANCE;
}
public synchronized StringBuilder acquire() {
StringBuilder sb = idle.pollFirst();
if (sb == null) {
sb = new StringBuilder(1024);
} else {
sb.setLength(0); // reset before reuse
}
return sb;
}
public synchronized void release(StringBuilder sb) {
if (sb == null) return;
if (idle.size() < maxSize) {
sb.setLength(0);
idle.addFirst(sb);
}
// else drop it; pool is full
}
}
This demonstrates the “extension” clearly: the pool is globally accessible through one broker, but the broker is not the goal—the reuse policy is.
Your text also hints at a different variation: “exactly two instances” (or “exactly N instances”). This is commonly called the Multiton pattern. It preserves controlled construction and controlled access, but it relaxes Singleton’s “exactly one” rule.
A Multiton is useful when you need a small, fixed set of coordinators (for example, a pair of queues, two modes, or per-tenant brokers), but you still want to prevent arbitrary construction.
To keep a Singleton truly unique, you must prevent clients from constructing, copying, or assigning additional instances. In C++, that means controlling constructors and disabling copying/assignment; in Java, it means a private constructor and careful handling of reflection/serialization if those apply.
// Example: prevent copying (C++)
// Singleton shifty(*Singleton::Instance()); // should be illegal if copy is disabled
Returning a pointer invites callers to delete it. Returning a reference reduces that temptation and clarifies ownership:
the Singleton owns itself and controls its lifetime.
// inside class Singleton (C++)
static Singleton& Instance();
Assignment does not create a second instance, but disabling it communicates intent and prevents misuse in APIs that imply multiple instances. In C++, a private destructor can prevent deletion through a pointer, but it also complicates teardown—so use it deliberately.
class Singleton {
public:
static Singleton& Instance();
// operations...
private:
Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton();
};
Modern C++ typically expresses these rules with = delete and uses a function-local static for thread-safe initialization.
The main idea is the same: one controlled instance, no accidental copies, and a clear ownership model.
Through-line: Now that you have seen how Singleton mechanics can be extended into “two instances” or “a reusable pool,” the exercise asks you to implement a controlled-creation class that follows those same rules.
In this exercise, you will write a class that is a variation of the Singleton.
Dual Pattern - Exercise
getter methods.