Patterns/Creational/Singleton
Please note that this post is kind of essay to gather info for presentation and mentoring of younger developers.
Patterns/Creational/Singleton
Purpose:
- Global access point to a single instance
- Ensure that a class has only one instance
Problems:
- Hard to test, because it is mostly impossible to mock
- Violates Single Responsibility Principle, IoC, and DI
Classic implementation:
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) instance = new Singleton();
return instance;
}
}
Modern implementation:
@Component
@Scope("singleton")
public class Singleton {
}
Actually, it is not a Singleton, but using of Factory that cache that single instance.
Use Cases
Infrastructure - Object Mapper
Let’s say that your application actively uses Jackson object mapper.
You can create a singleton object mapper to:
- avoid creating a new instance every time you need to serialize or deserialize an object.
- avoid redundant dependencies in constructors, where you need to pass the object mapper mostly to all instances.
public class JsonMapper {
public static final JsonMapper JSON = new JsonMapper();
private final ObjectMapper mapper = new ObjectMapper();
public String asString(Object object) {
return mapper.writeValueAsString(object);
}
}
@Slf4j
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
class UserController {
private final UserService userService;
@GetMapping
public List<UserRow> getUser(Filter filter) {
log.info("Getting user with filter: {}", JSON.asString(filter));
}
}
Here, we use the JsonMapper as a kind of infrastructure for all classes, by this reason it may be logical to exclude it from the constructor dependencies and make it a singleton.
Shortcut for non-important params
Let’s assume that your application has headers that are received from the client, but should be forwarded to other services. Of course, you can pass it as the arguments, but generally it is not necessary to have it in the method signature.
You can create a singleton context objects that will hold these headers.
public class TraceContext {
public static final TraceContext TRACE_CONTEXT = new TraceContext();
private final ThreadLocal<String> headers = new ThreadLocal<>();
public void addTraceId(String value) {
headers.set(value);
}
public String getTraceId() {
return headers.get();
}
public void clear() {
headers.remove();
}
}
@Controller
@RequestMapping("/user")
@RequiredArgsConstructor
class UserController {
private final UserService userService;
@GetMapping
public List<UserRow> getUser(Filter filter, @Haeder("X-Tracing-Id") String tracingId) {
TRACE_CONTEXT.addTraceId(tracingId);
List<UserRow> users = this.userService.getUsers(filter);
TRACE_CONTEXT.clear();
return users;
}
}
@Component
class UserExternalService {
public List<User> loadRemoteUsers(List<String> userIds) {
String traceId = TRACE_CONTEXT.getTraceId();
// Request to external service
}
}
That example is also aim to simplify code by hiding some technical/infrastructure details from the business logic. Here we set trace id in the controller, but it could be done also using filters or interceptors.
However, you should be careful because it may cause some issues if you would use it in the multithreading environment. Also, it may be easily replaced with Spring Injections.
Common resource - Transaction Log
Let’s assume that your application has a transaction log that should be written to the single file.
You can create a singleton logger that will hold the file writer.
public class TransactionLoggerSingleton {
public static final TransactionLoggerSingleton TX_LOG = TransactionLoggerSingleton();
private final FileWriter fileWriter;
private final PrintWriter printWriter;
private TransactionLoggerSingleton() throws IOException {
this.fileWriter = new FileWriter("path/to/your/transactions.log", true);
this.printWriter = new PrintWriter(fileWriter);
}
public void logTransaction(String transactionRecord) {
synchronized (this) {
printWriter.println(transactionRecord);
printWriter.flush();
}
}
}
@Service
class UserService {
public void saveUser(User user) {
TX_LOG.logTransaction("User saved: " + user);
// ...
}
}
That example easily could be replaced with Spring Injections, however, for applications without IoC container it could be a good way to simplify the code.
Different Implementations
- Eager initialization
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- Lazy locking
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- Lazy initialization with double-checked locking
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- Lazy initialization with holder idiom
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
- Enum
public enum Singleton {
INSTANCE
}
How Actually Lazy Initialization Works
Let’s examine how does eager initialization work:
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
System.out.println("Singleton created");
}
public static Singleton getInstance() {
return instance;
}
}
We will use it in the following way:
public class Main {
public static void main(String[] args) {
System.out.println("Start");
System.out.println(Singleton.class.getCanonicalName());
System.out.println("End");
}
}
The main purpose of Lazy Singleton is to ensure that the instance is created only when it is needed. Here we load the class in our application, but code will cause the next output:
Start
Singleton
End
(Yes, I also was surprised when I saw it first time, but it is true at least with my JDK.)
So, it seems that lazy initialization is not needed. But let’s scan fields using reflection:
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
System.out.println("Start");
Stream.of(Singleton.class.getDeclaredFields())
.forEach(System.out::println);
System.out.println("End");
}
}
And the output will be:
Start
private static final Singleton Singleton.instance
End
And once again, even scanning by Reflections doesn’t cause the creation of the Singleton instance. Only calling some
static method or access to static field (.class
is not counted) will cause the creation of the Singleton instance.
Therefore, the lazy initialization seems useless until you have no additional public static fields or methods in your singleton class. What is honestly rare in the real world.