Certain objects within a software system, such as application settings, should ideally have only one instance. Creating multiple instances leads to unnecessary memory consumption and resource waste. The following example demonstrates a standard configuration loader without singleton constraints.
Properties File (app.settings):
apiKey = XYZ123
maxConnections = 50
Standard Configuration Reader:
package core.config;
import java.io.*;
import java.util.Properties;
public class SystemSettings {
private String apiKey;
private String maxConn;
public SystemSettings() {
loadProperties();
}
private void loadProperties() {
Properties props = new Properties();
try (InputStream input = getClass().getResourceAsStream("app.settings")) {
if (input == null) {
System.out.println("Sorry, unable to find app.settings");
return;
}
props.load(input);
this.apiKey = props.getProperty("apiKey");
this.maxConn = props.getProperty("maxConnections");
} catch (IOException ex) {
ex.printStackTrace();
}
}
public String getApiKey() { return apiKey; }
public String getMaxConn() { return maxConn; }
}
Client Usage:
package core.config;
public class AppRunner {
public static void main(String[] args) {
SystemSettings settings1 = new SystemSettings();
SystemSettings settings2 = new SystemSettings();
System.out.println(settings1.getApiKey());
System.out.println(settings2.getApiKey());
}
}
The issue with this approach is that every invocation of new SystemSettings() triggers I/O operations and creates a distinct object in memory, even though the data remains identical. A more efifcient approach is to ensure the class is instantiated only once.
Definition: The Singleton Pattern ensures a class has only one instance and provides a global point of access to it.
Eager Initialization (The "Eager" Approach)
In this variant, the instance is created at the time the class is loaded. This is straightforward and thread-safe by default.
package core.config;
import java.io.*;
import java.util.Properties;
public class EagerSettings {
// Instance created immediately when the class is loaded
private static final EagerSettings INSTANCE = new EagerSettings();
private String apiKey;
private String maxConn;
// Private constructor prevents external instantiation
private EagerSettings() {
loadProperties();
}
private void loadProperties() {
Properties props = new Properties();
try (InputStream input = getClass().getResourceAsStream("app.settings")) {
if (input != null) {
props.load(input);
this.apiKey = props.getProperty("apiKey");
this.maxConn = props.getProperty("maxConnections");
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
public static EagerSettings getInstance() {
return INSTANCE;
}
public String getApiKey() { return apiKey; }
public String getMaxConn() { return maxConn; }
}
Lazy Initialization (The "Lazy" Approach)
Here, the instance is created only when its first requested. The basic implementation below is not thread-safe.
package core.config;
import java.io.*;
import java.util.Properties;
public class LazySettings {
private static LazySettings instance;
private String apiKey;
private String maxConn;
private LazySettings() {
loadProperties();
}
private void loadProperties() {
// ... loading logic similar to above ...
}
public static LazySettings getInstance() {
if (instance == null) {
instance = new LazySettings();
}
return instance;
}
}
Thread-Safe Lazy Initialization
To make the lazy approach safe for multithreaded environments, synchronization is required. A common way is using the synchronized keyword or volatile.
package core.config;
public class ThreadSafeSettings {
private static volatile ThreadSafeSettings instance;
private String apiKey;
private String maxConn;
private ThreadSafeSettings() {
// Initialization logic
}
public static ThreadSafeSettings getInstance() {
if (instance == null) {
synchronized (ThreadSafeSettings.class) {
if (instance == null) {
instance = new ThreadSafeSettings();
}
}
}
return instance;
}
}
Comparison: Eager vs. Lazy
| Feature | Eager Initialization | Lazy Initialization |
|---|---|---|
| Instance Creation | At class loading time. | When getInstance() is first called. |
| Performance | Faster access (no null checks/locking), but higher startup memory. | Slower first access (due to creation), saves memory if never used. |
| Thread Safety | Inherently safe (Classloader guarantee). | Requires manual handling (synchronization). |
Variant: Limited Instance Pool
The concept of Singleton can be extended to control a specific number of instances (e.g., limiting to 3 objects) using a registry.
package core.config;
import java.util.HashMap;
import java.util.Map;
public class PooledConfig {
private static final int MAX_INSTANCES = 3;
private static Map<Integer, PooledConfig> instanceMap = new HashMap<>();
private static int counter = 1;
private PooledConfig() { }
public static PooledConfig acquire() {
if (!instanceMap.containsKey(counter)) {
instanceMap.put(counter, new PooledConfig());
}
PooledConfig current = instanceMap.get(counter);
counter++;
if (counter > MAX_INSTANCES) {
counter = 1;
}
return current;
}
}
Running a loop to acquire 10 instances of PooledConfig will result in only 3 distinct objects being created, cycling through them based on the counter logic.