Vài điều về Singleton

Warm up

Khi công ty ngày càng phát triển và cung cấp dịch vụ rộng rãi, số lượng shipper tăng đều. Để thuận tiện quản lý, ta có phần mềm quản lý riêng shipper, với mỗi shipper sẽ có một object tương ứng tạo ra. Sẽ thế nào nếu có xxx object cùng kết nối đến database? Làm thế nào để đảm bảo cho việc chỉ có 1 kết nối đến database và các tác vụ được thực hiện tuần tự, truy cập được đồng bộ?
Singleton lúc này sẽ là một giải pháp hữu hiệu. Vậy Singleton là gì? Singleton cần đảm bảo những gì? Triển khai Singleton như thế nào? 
Bài viết này sẽ giải đáp những câu hỏi trên về Singleton.

Singleton là gì?

Design Patterns, những giải pháp tổng quát nhất có thể tái sử dụng cho các trường hợp khi cần thiết kế kiến trúc phần mềm. Singleton là một trong số đó, thuộc nhóm khởi tạo — Creational Patterns.
Singleton đối ứng với các cơ chế tạo và kiểm soát việc tạo đối tượng. Đúng như cái tên “đơn độc” của mình, Singleton đảm bảo cho mỗi class chỉ có duy nhất một thể hiện được khởi tạo và được truy xuất mọi nơi.
Đối với ví dụ trên, Singleton sẽ chỉ tạo ra một thể hiện của đối tượng — instance — khởi tạo duy nhất một lần nhưng có khả năng truy xuất và sử dụng mọi nơi.
Trực quan hơn ta có một biểu đồ UML thể hiện Singleton:

Singleton UML Diagram

Vậy Singleton có gì?

Triển khai Singleton có khá nhiều cách tuy nhiên cách nào cũng cần đảm bảo những yếu tố sau mới đảm bảo class đó là class Singleton:
+ Private static field — một trường static chỉ có khả năng truy cập trong class để khởi tạo instance, đảm bảo cho tính duy nhất và chỉ được tạo trong class đó của instance.
+ Private constructor — một phương thức khởi tạo cũng hạn chế khả năng truy cập từ bên ngoài, ngăn việc tạo instance từ bên ngoài.
+ Public method — một phương thức trả về instance không hạn chế khả năng truy cập từ đó có thể truy xuất và sử dụng instance mọi nơi.
Triển khai Singleton thế nào?
Như đã nói ở trên, có rất nhiều cách để triển khai Singleton nhưng trong bài viết này, ta sẽ đề cập đến những cách thức triển khai đơn giản, chung nhất cho Singleton.
a. Eager Initialization:
Kiểu khởi tạo “xông xáo”, nghĩa là không cần biết em là ai, không cần biết em từ đâu, nhưng vẫn quyết tâm khởi tạo ra một instance ngay khi class được load lần đầu.
Đây là cách khởi tạo đơn giản nhất, nhanh đối với instance không quá tốn nhiều tài nguyên. Nên từ đó phát sinh những hạn chế như sau:
+ Instance tạo ra có thể không cần dùng đến.
+ Performance thấp, tốn tài nguyên do khởi tạo instance ngay khi class được load.
+Không có cách nào để bắt lỗi và xử lý lỗi (exception) trong quá trình khởi tạo.
Ví dụ về cách khởi tạo “xông xáo”:

public class Student {
private static final Student INSTANCE = new Student();
private Student(){
}
public static Student getInstance() {
return INSTANCE;
}
}

b. Static block Initialization:
Tương tự như Eager Init nhưng cách triển khai này có thêm một static block cung cấp xử lý ngoại lệ trong quá trình khởi tạo. Dù khắc phục được nhược điểm về bắt và xử lý lỗi của Eager nhưng Static Block vẫn còn hạn chế về performance cũng như tính hữu dụng của instance.

public class Student {
private static Student instance;
private Student(){
}
static{
try{
instance = new Student();
} catch(Exception e){
throw new RuntimeException(“Exception occured in creating singleton instance);
}
}
public static Student getInstance() {
return instance;
}
}

c. Lazy Initialization
Đây là cách triển khai mở rộng, giải quyết hạn chế cho hai cách triển khai trên nhưng chỉ hoạt động tốt với Thread đơn lẻ. 
Kiểu khởi tạo “lười” này chỉ tạo instance khi cần dùng đến. Nghĩa là việc khởi tạo và trả về instance sẽ cùng thực hiện trong method được cung cấp khi method đó được gọi.
Tránh được hạn chế về xử lý ngoại lệ, cải thiện được performance và tối ưu instance được tạo thì Lazy lại gặp lỗi với đa luồng (multi thread) do method có thể khởi tạo cùng lúc nhiều đối tượng ở các luồng khác nhau, phá vỡ tính chất của Singleton.

public class Student {
private static Student instance;
private Student(){
}
public static Student getInstance() {
if(instance == null){
instance = new Student();
}
return instance;
}
}

d. Thread Safe Singleton:
Thread safe — luồng an toàn — sinh ra như một cách giải quyết vấn đề cho Lazy, Thread Safe làm việc với đa luồng. Thread Safe triển khai tương tự Lazy nhưng trong phương thức truy cập được thêm vào từ khóa synchronized, đồng bộ hóa quá trình truy cập và khởi tạo, tạo ra sự tuần tự, luồng nào đến trước sẽ được xử lý trước.
Tuy nhiên thread safe có performance thấp do synchronized bao quát tất cả các quá trình có trong phương thức nên làm chậm quá trình truy xuất instance.

public class Student {
private static Student instance;
private Student(){
}
public static synchronized Student getInstance() {
if(instance == null){
instance = new Student();
}
return instance;
}
}

Để tối ưu hiệu suất (Performance):

 public class Student {
private static Student instance;
private Student(){
}
public static Student getInstance() {
if(instance == null){
synchronized(Student.class){
instance = new Student();
}
}
return instance;
}
}

e. Bill Pugh Implementation:
Phương pháp này do Bill Pugh triển khai dựa trên cơ chế static nested class. Ông tạo ra một lớp Helper private ngay trong class Singleton, tăng tính bao gói dữ liệu giữa các lớp, thuận tiện hơn cho việc đọc và bảo trì code. Khi Singleton được tải vào bộ nhớ thì SingletonHelper chưa được tải vào. Khi và chỉ khi phương thức trả về được gọi, SingletonHelper mới được tải và tạo ra instance.
Cách làm của Bill Pugh tránh được lỗi cơ chế khởi tạo instance của Singleton trong Multi Thread, performance cao do tách biệt được quá trình xử lý. Cách làm này được đánh giá là cách triển khai Singleton nhanh và hiệu quả nhất.

public class Student {
//static nested class
private static class SingletonHelper {
private static final Student INSTANCE = new Student();
}
private Student(){
}
public static Student getInstance() {
return SingletonHelper.INSTANCE;
}
}

Ngoài ra còn có các cách triển khai nâng cao với Enum Singleton, Serializable hoặc phá vỡ cấu trúc Singleton với Reflection API nhưng ta sẽ không triển khai chi tiết trong bài viết này.

Lời kết

Singleton trên thực tế thường dùng để thiết kế hay tạo ra đối tượng implement cho các class Logger, Cache, Connect Pool, Thread Pool,… — là những đối tượng chỉ cần tạo ra một lần, không cần thiết tạo thêm các thể hiện khác.

Singleton có quan hệ tốt với các Patterns khác, góp phần xây dựng pattern như: Abstract Factory, Builder, Facade, Prototype,…

Với sự “đơn thương độc mã” quyền lực của mình, Singleton là một mẫu thiết kế quan trọng trong thiết kế cấu trúc phần mềm, phát huy tốt cho những tình huống cần quản lý khởi tạo và truy cập của đối tượng.

Để tìm hiểu thêm về Singleton hay Design Pattern, bạn có thể tìm đọc sâu hơn trong cuốn Gang of Four Design Pattern hay Design Pattern For Dummies.