Singleton là một trong design pattern thuộc nhóm creational (khởi tạo) của bộ tứ huyền thoại Erich Gamma, Richard Helm, Ralph Johnson, và John Vlissides (Gang of Four, hay GoF). Chỉ nghe qua tên thì đây có vẻ là một design pattern đơn giản, nhưng khi đi vào triển khai thực tế, nó lại nảy sinh nhiều vấn đề khác nhau cần cân nhắc.
Trong bài viết này, chúng ta sẽ tìm hiểu về các nguyên tắc của kiểu thiết kế này, các cách cách áp dụng Singleton trong Java, và một số lưu ý để sử dụng nó một cách tốt nhất.
Nguyên tắc của Singleton
- Singleton hạn chế việc khởi tạo (instantiation) của một class (lớp), đảm bảo rằng chỉ có một thực thể (instance) duy nhất của lớp đó tồn tại trong máy ảo Java.
- Lớp Singleton phải cung cấp một điểm truy cập global để lấy instance của lớp.
- Singleton được sử dụng cho logging, các đối tượng driver, caching và thread pool.
- Singleton cũng được sử dụng trong các design pattern khác như Abstract factory, Builder, Prototype, Facade, v.v.
- Singleton cũng được dùng trong các class của core Java (ví dụ: java.lang.Runtime, java.awt.Desktop).
Áp dụng Singleton trong Java
Để triển khai design pattern này, chúng ta có nhiều cách tiếp cận khác nhau, nhưng tất cả đều dựa trên các khái niệm chung sau:
- Constructor dạng private để hạn chế việc khởi tạo lớp từ các lớp khác.
- Biến static private thuộc chính lớp đó, là instance duy nhất của lớp.
- Phương thức dạng public static trả về instance của lớp, đây là điểm truy cập global để bên ngoài có thể lấy instance của class singleton.
Trong các phần tiếp theo, chúng ta sẽ tìm hiểu các cách khác nhau để triển khai kiểu thiết kế Singleton và các vấn đề cần lưu ý.
Khởi tạo tức thời (Eager initialization)
Với phương pháp eager initialization, instance của lớp Singleton được tạo ngay tại thời điểm nạp lớp (class loading). Nhược điểm của cách khởi tạo này là instance được tạo ra ngay cả khi ứng dụng client có thể chưa sử dụng đến nó.
Dưới đây là cách triển khai Singleton sử dụng khởi tạo static:
Chúng ta nên tránh việc khởi tạo instance trừ khi client gọi phương thức getInstance. Ngoài ra, cách này không cung cấp tùy chọn nào cho việc xử lý lỗi (exception handling).
Khởi tạo bằng block static (Static block initialization)
Cách triển khai này tương tự như eager initialization. Điểm khác biệt là instance của lớp được tạo trong một block static, cho phép thêm tùy chọn xử lý lỗi.
Khởi tạo trì hoãn (Lazy Initialization)
Trong phương pháp này, instance của lớp Singleton sẽ được tạo bên trong phương thức truy cập global.
Dưới đây là code ví dụ cho việc tạo lớp singleton theo cách này:
Trong phần tiếp theo, chúng ta sẽ xem xét các cách khác nhau để tạo ra một lớp singleton an toàn luồng (thread-safe).
Singleton An toàn luồng (Thread Safe Singleton)
Một cách đơn giản để tạo lớp singleton an toàn luồng là đặt từ khóa synchronized cho phương thức truy cập global để đảm bảo tại bất kì thời điểm nào chỉ có tối đa một thread có thể thực thi phương thức đó.
Dưới đây là cách triển khai chung của phương pháp này:
Để tránh gánh nặng xử lý không cần thiết này mỗi lần gọi, chúng ta sử dụng nguyên tắc double-checked locking. Trong phương pháp này, khối code synchronized được sử dụng bên trong câu lệnh if, cùng với một lần kiểm tra bổ sung, để đảm bảo chỉ một instance của lớp singleton được tạo ra.
Đoạn code sau minh họa cách triển khai double-checked locking:
Triển khai Singleton của Bill Pugh
Trước Java phiên bản 5, mô hình bộ nhớ Java có nhiều vấn đề, và các cách tiếp cận ở trên thường thất bại trong một số trường hợp khi có quá nhiều thread cố gắng lấy instance của lớp singleton cùng một lúc.
Do đó, Bill Pugh (một nhà khoa học máy tính nổi tiếng) đã đề xuất một cách khác để tạo lớp singleton bằng cách sử dụng một lớp helper static nội bộ (inner static helper class).
Ví dụ về cách triển khai Singleton của Bill Pugh:
Chỉ khi getInstance() được gọi, lớp này mới được load và tạo ra instance của lớp Singleton. Đây là phương thức Singleton phổ biến nhất vì nó không yêu cầu đồng bộ hóa.
Dùng Reflection để phá vỡ thiết kế Singleton
Reflection có thể được sử dụng để phá vỡ tất cả các cách triển khai singleton đã được trình bày. Dưới đây là một lớp ví dụ:
Enum Singleton
Để khắc phục vấn đề này với Reflection, Joshua Bloch đề xuất sử dụng enum để triển khai kiểu thiết kế Singleton. Lý do là Java đảm bảo rằng mỗi giá trị enum chỉ được khởi tạo một lần duy nhất trong chương trình, mà các giá trị Java Enum có thể truy cập global, nên instance Singleton cũng vậy.
Nhược điểm là type enum khá kém linh hoạt (một ví dụ là nó không cho phép khởi tạo trì hoãn – lazy initialization).
Serialization và Singleton
Đôi khi trong các hệ thống phân tán (distributed), chúng ta cần triển khai interface Serializable cho lớp Singleton để có thể lưu trạng thái của nó vào file và khôi phục lại vào thời điểm sau đó.
Dưới đây là một lớp Singleton nhỏ cũng triển khai interface Serializable:
instanceOne hashCode=2011117821
instanceTwo hashCode=109647522
Như vậy, nó đã phá vỡ kiểu thiết kế Singleton. Để khắc phục tình huống này, tất cả những gì chúng ta cần làm là cung cấp hàm readResolve().
protected Object readResolve() {
return getInstance();
}
Sau khi làm điều này, bạn sẽ thấy hashCode của cả hai instance trong chương trình test là giống nhau.
Kết luận
Bài viết này đã trình bày về mẫu thiết kế Singleton. Hy vọng những thông tin trên sẽ hữu ích cho bạn trong quá trình học lập trình Java. Đừng ngần ngại khám phá thêm các bài hướng dẫn Java khác của chúng tôi để mở rộng kiến thức và nâng cao kỹ năng nhé!