Singleton là một mẫu thiết kế thường được sử dụng nhằm đảm bảo rằng một lớp chỉ có duy nhất một thể hiện (instance) và cung cấp một điểm truy cập toàn cục đến nó.
Tuy nhiên, quá trình tuần tự hóa (serialization) có thể âm thầm phá vỡ cam kết của Singleton bằng cách tạo ra một thể hiện mới trong quá trình giải tuần tự hóa (deserialization). Trong bài viết này, chúng ta sẽ tìm hiểu lý do tại sao điều này xảy ra và cách khắc phục.
Vấn đề: Serialization phá vỡ Singleton
Hãy bắt đầu với một lớp Singleton cơ bản:
import java.io.*; class Singleton implements Serializable { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; }
}
Bây giờ hãy serialize và deserialize Singleton này:
Singleton instance1 = Singleton.getInstance(); // Serialize to a file
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser"));
out.writeObject(instance1);
out.close(); // Deserialize from the file
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser"));
Singleton instance2 = (Singleton) in.readObject();
in.close(); System.out.println(instance1 == instance2); // false ❌
Mặc dù Singleton đã được cài đặt đúng, nhưng quá trình giải tuần tự hóa lại tạo ra một đối tượng mới, phá vỡ tính chất Singleton.
Cách khắc phục: Cài đặt phương thức readResolve()
Java cung cấp một hook có tên là readResolve()
cho phép bạn kiểm soát đối tượng được trả về trong quá trình giải tuần tự hóa.
Sửa lại lớp Singleton như sau:
class Singleton implements Serializable { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } // This ensures that deserialization returns the original instance private Object readResolve() throws ObjectStreamException { return instance; }
}
Bây giờ, quá trình serialize và deserialize sẽ giữ đúng thể hiện Singleton:
System.out.println(instance1 == instance2); // true ✅
Ghi chú
- Phương thức
readResolve()
phải là private và trả về Object. - Nếu không có nó, cơ chế serialization mặc định của Java sẽ tạo ra một thể hiện mới.
- Luôn kiểm tra kỹ các lớp Singleton nếu chúng cài đặt
Serializable
.
Bonus: Ngăn chặn các cuộc tấn công thông qua Reflection (Không bắt buộc)
Serialization không phải là mối đe dọa duy nhất với Singleton. Reflection cũng có thể phá vỡ nó.
Bạn có thể ném ra một ngoại lệ trong constructor nếu thể hiện đã tồn tại:
private Singleton() { if (instance != null) { throw new RuntimeException("Use getInstance() method to get the single instance of this class"); }
}
Kết luận
Serialization có thể làm suy yếu mẫu thiết kế Singleton, nhưng readResolve()
là một công cụ đơn giản mà hiệu quả để duy trì tính toàn vẹn của Singleton.
Hãy luôn cẩn thận khi đánh dấu một lớp Singleton là Serializable
, đặc biệt trong các hệ thống phân tán, bộ nhớ cache, hoặc khi truyền đối tượng qua mạng.