Triển khai mẫu Singleton trong Java: Từ cơ bản đến tối ưu hóa đồng thời

Mẫu Singleton là một trong những mẫu thiết kế nền tảng nhằm đảm bảo rằng một lớp chỉ có đúng một thể hiện duy nhất trong suốt vòng đời ứng dụng, đồng thời cung cấp một điểm truy cập toàn cục đến thể hiện đó. Mẫu này đặc biệt hữu ích khi quản lý tài nguyên chung như kết nối cơ sở dữ liệu, bộ đệm toàn cục, hoặc cấu hình hệ thống.

Các phương pháp khởi tạo chính

1. Khởi tạo ngay lập tức (Eager Initialization)
Thể hiện được tạo khi lớp được tải vào bộ nhớ — không cần kiểm tra điều kiện hay đồng bộ hóa. Ưu điểm nổi bật là tính an toàn luồng tuyệt đối và đơn giản; nhược điểm là thể hiện được tạo dù chưa từng được sử dụng.

public class ConfigManager {
    private static final ConfigManager INSTANCE = new ConfigManager();

    private ConfigManager() {
        // Khởi tạo các tham số hệ thống
    }

    public static ConfigManager get() {
        return INSTANCE;
    }
}

2. Khởi tạo trì hoãn với khóa toàn cục (Synchronized Lazy Init)
Thể hiện chỉ được tạo khi lần đầu gọi get(). Tuy nhiên, việc đồng bộ hóa toàn bộ phương thức gây tổn thất hiệu năng do mọi cuộc gọi — kể cả sau khi thể hiện đã tồn tại — đều phải chờ khóa.

public class Logger {
    private static Logger instance;

    private Logger() {}

    public static synchronized Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }
}

Cải tiến nâng cao: Khởi tạo trì hoãn có kiểm tra kép

Đây là cách triển khai cân bằng giữa hiệu năng và tính an toàn luồng. Nó loại bỏ chi phí đồng bộ hóa không cần thiết sau lần khởi tạo đầu tiên bằng cách áp dụng hai lớp kiểm tra — ngoài khối synchronized và bên trong nó — kết hợp với từ khóa volatile để ngăn chặn tái sắp xếp lệnh và đảm bảo khả năng nhìn thấy biến trên mọi luồng.

public class DatabaseConnectionPool {
    private static volatile DatabaseConnectionPool pool;

    private DatabaseConnectionPool() {
        // Thiết lập kết nối ban đầu
    }

    public static DatabaseConnectionPool acquire() {
        if (pool == null) {
            synchronized (DatabaseConnectionPool.class) {
                if (pool == null) {
                    pool = new DatabaseConnectionPool();
                }
            }
        }
        return pool;
    }
}

Đặc trưng cốt lõi

  • Tính duy nhất toàn cục: Mỗi lần gọi phương thức truy cập trả về cùng một tham chiếu đối tượng.
  • Khởi tạo theo nhu cầu: Không tiêu tốn tài nguyên cho các lớp chưa được sử dụng.
  • Tương thích đa luồng: Triển khai đúng chuẩn tránh tình trạng tạo nhiều thể hiện do cạnh tranh giữa các luồng.

Trường hợp sử dụng điển hình

  • Quản lý cấu hình ứng dụng (file application.properties được đọc một lần và chia sẻ).
  • Hệ thống ghi nhật ký tập trung với bộ đệm và luồng ghi riêng.
  • Bộ quản lý phiên làm việc (session manager) trong ứng dụng web.

Vấn đề cần lưu ý

Khi lớp Singleton hỗ trợ tuần tự hóa (serialization), cần ghi đè phương thức readResolve() để đảm bảo rằng việc khôi phục từ byte stream không tạo ra thể hiện mới:

private Object readResolve() {
    return get(); // Luôn trả về thể hiện duy nhất
}

Ngoài ra, với các ứng dụng sử dụng framework như Spring, nên ưu tiên dùng scope @Scope("singleton") thay vì triển khai thủ công, vì container đã xử lý đầy đủ các vấn đề về chu kỳ sống, tiêm phụ thuộc và an toàn luồng.

Thẻ: Java design-patterns singleton thread-safety Serialization

Đăng vào ngày 15 tháng 6 lúc 03:56