1. Động cơ sử dụng Singleton
Trong quá trình phát triển phần mềm, có những lớp mà chúng ta chỉ cần một phiên bản duy nhất hoạt động trong toàn bộ hệ thống. Hãy xem xét một ví dụ quen thuộc: Trình quản lý tác vụ (Task Manager) của Windows. Dù bạn có nhấp chuột phải vào thanh tác vụ và chọn "Start Task Manager" bao nhiêu lần đi nữa, hệ thống cũng chỉ hiển thị một cửa sổ duy nhất. Tại sao lại như vậy?
- Tiết kiệm tài nguyên: Nếu có nhiều cửa sổ giống hệt nhau, mỗi cửa sổ sẽ phải truy vấn thông tin hệ thống (CPU, RAM, v.v.), gây lãng phí tài nguyên một cách không cần thiết.
- Đảm bảo tính nhất quán dữ liệu: Hai cửa sổ hiển thị các thông số khác nhau (ví dụ: CPU 10% và 15%) sẽ gây nhầm lẫn cho người dùng về trạng thái thực tế của hệ thống.
Chính vì vậy, trong thiết kế phần mềm, chúng ta cần một cơ chế để đảm bảo một lớp chỉ có một thể hiện duy nhất. Mẫu thiết kế Singleton ra đời để giải quyết vấn đề này.
2. Tổng quan về Singleton
Hãy cùng xây dựng một lớp mô phỏng Trình quản lý tác vụ. Lớp TaskManager ban đầu có thể trông như thế này:
class TaskManager {
public TaskManager() { ... }
public void displayProcesses() { ... }
public void displayServices() { ... }
}
Để biến nó thành một Singleton, chúng ta thực hiện ba bước sau:
- Ẩn hàm tạo: Đánh dấu hàm tạo là
privateđể ngăn không cho tạo đối tượng từ bên ngoài lớp. - Tạo một biến tĩnh nội bộ: Khai báo một biến
private staticcủa chính lớp đó để lưu trữ thể hiện duy nhất. - Cung cấp phương thức truy cập tĩnh: Định nghĩa một phương thức
public static(thường gọi làgetInstance()) để trả về thể hiện duy nhất này.
class TaskManager {
private static TaskManager instance = null;
private TaskManager() { ... }
public static TaskManager getInstance() {
if (instance == null) {
instance = new TaskManager();
}
return instance;
}
public void displayProcesses() { ... }
public void displayServices() { ... }
}
Giờ đây, ở bất kỳ đâu trong chương trình, thay vì new TaskManager(), chúng ta gọi TaskManager.getInstance(). Lần gọi đầu tiên sẽ tạo ra đối tượng, các lần gọi sau sẽ trả về chính đối tượng đã tạo.
3. Ví dụ thực tế: Bộ cân bằng tải (Load Balancer)
Một công ty phần mềm cần phát triển một bộ cân bằng tải. Bộ cân bằng tải này phải là duy nhất trong hệ thống để đảm bảo việc phân phối yêu cầu từ client đến các máy chủ trong cụm được nhất quán.
import java.util.*;
class LoadBalancer {
private static LoadBalancer instance = null;
private List<String> serverList = null;
private LoadBalancer() {
serverList = new ArrayList<>();
}
public static LoadBalancer getInstance() {
if (instance == null) {
instance = new LoadBalancer();
}
return instance;
}
public void addServer(String server) {
serverList.add(server);
}
public void removeServer(String server) {
serverList.remove(server);
}
public String getRandomServer() {
Random random = new Random();
int i = random.nextInt(serverList.size());
return serverList.get(i);
}
}
class Client {
public static void main(String[] args) {
LoadBalancer lb1 = LoadBalancer.getInstance();
LoadBalancer lb2 = LoadBalancer.getInstance();
LoadBalancer lb3 = LoadBalancer.getInstance();
// Kiểm tra tính duy nhất
if (lb1 == lb2 && lb2 == lb3) {
System.out.println("Bộ cân bằng tải là duy nhất!");
}
lb1.addServer("Server A");
lb1.addServer("Server B");
lb1.addServer("Server C");
for (int i = 0; i < 10; i++) {
System.out.println("Yêu cầu được gửi tới: " + lb1.getRandomServer());
}
}
}
4. Các biến thể của Singleton: Hungry vs Lazy
4.1. Singleton kiểu Eager (Khởi tạo sẵn - Hungry)
Trong biến thể này, đối tượng được tạo ra ngay khi lớp được tải vào bộ nhớ.
class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() {
return instance;
}
}
Ưu điểm: An toàn với đa luồng, tốc độ truy cập nhanh.
Nhược điểm: Không hỗ trợ "lazy loading", đối tượng luôn tồn tại trong bộ nhớ dù có được sử dụng hay không.
4.2. Singleton kiểu Lazy (Khởi tạo chậm - Lazy)
Biến thể này (ví dụ ở mục 3) chỉ tạo đối tượng khi phương thức getInstance() được gọi lần đầu tiên. Tuy nhiên, nó không an toàn trong môi trường đa luồng. Có thể khắc phục bằng từ khóa synchronized:
class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() { }
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
Cách này đảm bảo an toàn nhưng gây ra vấn đề về hiệu năng do mỗi lần gọi đều phải kiểm tra khóa. Giải pháp tốt hơn là Double-Checked Locking kết hợp với từ khóa volatile:
class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() { }
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
4.3. Singleton với static inner class (IoDH - Initialization on Demand Holder)
Đây là cách kết hợp ưu điểm của cả hai biến thể trên: cho phép "lazy loading" và an toàn với đa luồng mà không cần dùng synchronized..
class Singleton {
private Singleton() { }
private static class Holder {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return Holder.instance;
}
}
Cơ chế hoạt động: Lớp ngoài Singleton được tải mà không tạo đối tượng. Chỉ khi getInstance() được gọi, lớp trong Holder mới được tải và đối tượng của lớp ngoài mới được khởi tạo. Java Virtual Machine (JVM) đảm bảo quá trình tải lớp và khởi tạo biến tĩnh là an toàn với đa luồng.
5. Tổng kết
- Ưu điểm: Kiểm soát việc truy cập vào thể hiện duy nhất, tiết kiệm tài nguyên (đặc biệt với các đối tượng nặng như kết nối database, file system).
- Nhược điểm: Khó mở rộng (do không có interface/abstract class), vi phạm nguyên lý Single Responsibility (vừa tạo đối tượng vừa thực hiện chức năng), dễ mất trạng thái nếu không cẩn thận trong môi trường garbage collection.
- Khi nào dùng: Khi cần một tài nguyên dùng chung (logger, bộ đếm, kết nối cơ sở dữ liệu), hoặc khi cần đảm bảo chỉ có một điểm điều khiển duy nhất.
IoDH thường được khuyên dùng trong các ứng dụng Java vì sự kết hợp hoàn hảo giữa "lazy loading" và thread-safety.