Singleton là một trong những design pattern dạng khởi tạo (creational) được sử dụng rộng rãi nhất. Mục đích của nó là hạn chế số lượng object được tạo ra bởi ứng dụng. Khi sử dụng Singleton trong môi trường đa luồng, việc đảm bảo an toàn luồng cho lớp Singleton (Thread Safe Singleton) trở nên cực kỳ quan trọng.
Trong các ứng dụng thực tế, những tài nguyên như kết nối đến cơ sở dữ liệu hay hệ thống thông tin doanh nghiệp (Enterprise Information Systems, EIS) thường có giới hạn và cần được sử dụng một cách khôn ngoan để tránh tình trạng cạn kiệt.
Để giải quyết vấn đề này, ta có thể áp dụng mẫu thiết kế Singleton. Chúng ta sẽ tạo một lớp bọc (wrapper class) cho tài nguyên và giới hạn số lượng object được tạo ra tại runtime chỉ còn một để đảm bảo an toàn luồng khi triển khai Singleton trong Java.
Cách triển khai Thread Safe Singleton trong Java
Nhìn chung, để tạo một lớp singleton, ta thường thực hiện các bước sau:
- Tạo constructor (hàm khởi tạo) dạng private để ngăn việc tạo object mới bằng toán tử
new
. - Khai báo một thực thể dạng private static của chính lớp đó.
- Cung cấp một phương thức dạng public static để trả về thực thể của lớp singleton. Nếu biến thực thể chưa được khởi tạo, phương thức này sẽ khởi tạo nó. Nếu không, nó sẽ trả về thực thể đã có.
Đoạn code dưới đây sử dụng các nguyên tắc trên.
package com.journaldev.designpatterns;
public class ASingleton {
private static ASingleton instance = null;
private ASingleton() {
}
public static ASingleton getInstance() {
if (instance == null) {
instance = new ASingleton();
}
return instance;
}
}
Ở đây, phương thức getInstance()
không có tính an toàn luồng. Nhiều thread có thể truy cập nó cùng một lúc. Khi biến instance chưa được khởi tạo, các thread đầu tiên có thể cùng đi vào khối lệnh if
và tạo ra nhiều thực thể khác nhau. Điều này làm phá vỡ mục đích của việc triển khai Singleton.
Những cách tạo an toàn luồng cho Singleton trong Java
Có ba cách để chúng ta có thể đảm bảo điều này.
- Tạo biến instance ngay tại thời điểm class được load
Ưu điểm:
- Đảm bảo an toàn luồng mà không cần đồng bộ.
- Dễ triển khai.
Nhược điểm:
- Tài nguyên được tạo sớm, có thể không bao giờ được dùng đến trong ứng dụng.
- Ứng dụng client không thể truyền tham số, do đó ta không thể tái sử dụng nó. Ví dụ, ta không thể có một lớp singleton dùng chung cho việc kết nối database, khi mà ứng dụng client cần cung cấp các thuộc tính của database server.
- Đồng bộ hóa cho phương thức getInstance()
Ưu điểm:
- Đảm bảo an toàn luồng.
- Ứng dụng client có thể truyền tham số.
- Đạt được lazy initialization (khởi tạo trễ).
Nhược điểm:
- Hiệu năng chậm do gánh nặng xử lý từ cơ chế locking (khóa).
- Việc đồng bộ hóa trở nên không cần thiết sau khi biến thực thể đã được khởi tạo.
- Sử dụng khối đồng bộ bên trong khối lệnh if và biến volatile
Ưu điểm:
- Đảm bảo an toàn luồng.
- Ứng dụng client có thể truyền tham số.
- Đạt được lazy initialization.
- Giảm tối đa gánh nặng xử lý của việc đồng bộ hóa và chỉ áp dụng cho vài thread đầu tiên khi biến còn
null
.
Nhược điểm:
- Tốn thêm một câu lệnh
if
.
Sau khi xem xét cả ba phương pháp trên, có thể thấy cách thứ ba là lựa chọn tối ưu nhất. Class đã chỉnh sửa sẽ trông như sau:
package com.journaldev.designpatterns;
public class ASingleton {
private static volatile ASingleton instance;
private static Object mutex = new Object();
private ASingleton() {
}
public static ASingleton getInstance() {
ASingleton result = instance;
if (result == null) {
synchronized (mutex) {
result = instance;
if (result == null)
instance = result = new ASingleton();
}
}
return result;
}
}
Biến cục bộ result
có vẻ không cần thiết ở đây, nhưng nó được dùng để cải thiện hiệu năng của code. Trong hầu hết các trường hợp, khi thực thể đã được khởi tạo, trường volatile chỉ bị truy cập một lần duy nhất (nhờ câu lệnh return result;
thay vì return instance;
).
Kỹ thuật này có thể cải thiện hiệu năng chung của phương thức lên đến 25%. Nếu bạn tin rằng có giải pháp nào tốt hơn, hoặc việc triển khai trên có thể ảnh hưởng đến tính an toàn luồng, hãy chia sẻ với chúng tôi biết bằng cách bình luận bên dưới.