Lesson 11 | Variations on the Singleton Pattern |
Objective | Write a Class that uses an Extension to the Singleton Pattern |
Write Class that uses an Extension to the Singleton Pattern
Write a variation of the Singleton pattern which stores all created objects in an internal list (using a LinkedList or ArrayList) in Java.
When a new thread is requested, the Java class returns any idle threads in the list.
If no idle threads are available, a new Thread is constructed and returned.
Object Pool Pattern
The object pool pattern is a software creational design pattern that uses a set of initialized objects kept ready to use, rather than allocating and destroying them on the fly. Here's an implementation of a simple thread pool that maintains a list of idle threads:
import java.util.LinkedList;
public class ThreadPool {
// LinkedList to store idle threads.
private LinkedList<Thread> idleThreads;
// Making the ThreadPool a Singleton
private static ThreadPool instance;
// Private constructor
private ThreadPool() {
this.idleThreads = new LinkedList<>();
}
// Public method to get the instance of the ThreadPool
public static synchronized ThreadPool getInstance() {
if (instance == null) {
instance = new ThreadPool();
}
return instance;
}
// Method to get a thread from the pool
public synchronized Thread getThread() {
if (!idleThreads.isEmpty()) {
return idleThreads.removeFirst();
} else {
// Create a new Thread if no idle threads are available
return new Thread();
}
}
// Method to return a thread to the pool
public synchronized void returnThread(Thread thread) {
idleThreads.add(thread);
}
}
In this code, we create a ThreadPool class that functions as a Singleton and maintains a LinkedList of idle Thread objects. The getThread() method returns an idle thread if one is available; otherwise, it creates a new one. The returnThread(Thread thread) method allows a thread to be returned to the pool of idle threads.
Do remember, this is a simplified implementation. Real-world thread pool managers need to handle more complicated situations, such as setting an upper limit on the number of threads, monitoring thread state, recycling dead threads, and so on. It's also important to consider thread safety when using such a class in a multithreaded environment.
Patterns are not written in stone and should adjust your application of patterns to meet your needs. Sometimes the techniques in a pattern can be tweaked to create slightly different but related patterns.
For example, suppose you need a class that has exactly two instances instead of exactly one. You can still use the tricks you learned with the Singleton pattern (a non-public
constructor[1] and a public static
accessor method[2] to do this.
You just need to change it slightly by passing some sort of argument to the accessor method that specifies which instance is required.
One possible use of this might be to implement a queued pool of objects.
For instance, Java threads, though lighter weight than processes, still have nontrivial overhead associated with their creation. If you need to spawn many of them (for instance, imagine a Web server that spawns a thread for each connection), it is often more efficient to reuse threads.
A variant of the Singleton pattern can be developed that stores all created objects in an internal list, and, when a new thread is requested,
returns any idle threads in the list. However, if no idle threads are available, a new thread is constructed and returned.
In many ways this pattern does not have the same motivation or purpose as the Singleton. However, it uses the essential ideas of the Singleton.
Enforcing the Singleton's Uniqueness
A few language-related techniques are of help in enforcing the Singleton's uniqueness. We have already used
a couple of them: The default constructor and the copy constructor are private.
The latter measure disables code such as this:
Singleton shifty(*Singleton::Instance()); // error!
// Cannot make 'shifty' a copy of the (Singleton) object returned by Instance
If you do not define the copy constructor, the compiler does its best to be of help and defines a public one
for you. Declaring an explicit copy constructor disables automatic generation, and placing
that constructor in the private section yields a compile-time error on shifty's definition.
Another slight improvement is to have Instance return a reference instead of a pointer. The problem
with having Instance return a pointer is that callers might be tempted to delete it. To minimize the
chances of that happening, it is safer to return a reference:
// inside class Singleton
static Singleton& Instance();
Another member function silently generated by the compiler is the assignment operator. Uniqueness is not directly related to assignment, but one obvious consequence of uniqueness is that you cannot assign one object to another because there aren't two objects to start with. For a Singleton object, any assignment is a self-assignment, which does not make much sense anyway; thus, it is worthwhile to disable the assignment operator (by making it private and never implementing it).
The last coat of the armor is to make the destructor private. This measure prevents clients that hold a pointer to the Singleton object from deleting it accidentally. After the enumerated measures are added, Singleton's interface looks like the following.
class Singleton {
Singleton& Instance();
// ----- operations ...
private:
Singleton();
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton();
};
Dual Pattern - Exercise
[1]constructor: A pseudo-method that creates an object.
[2]accessor method: A method which allows other classes to read (but not change) the values of the attributes. Accessor methods are also known as
getter methods
. Accessor methods are public methods that merely return the value of the attribute. See also getter method.