Singleton pattern là gì?
Singleton pattern là gì?
Singleton pattern là 1 trong 5 design pattern thuộc nhóm Creational Design Pattern. Singleton pattern đảm bảo chỉ duy nhất một thể hiện (instance) được tạo ra và bạn có thể truy xuất được thể hiện duy nhất đó mọi lúc mọi nơi trong chương trình.
Singleton có 1 instance duy nhất và được các class khác gọi tới
Singleton pattern dùng để làm gì?
Việc sử dụng Singleton pattern sẽ giúp chúng ta đảm bảo những điều sau đây:
-
Đảm bảo chỉ có 1 thể hiện (instance) của lớp (class).
-
Giúp quản lý việc truy cập tốt hơn vì chỉ có 1 thể hiện (instance) duy nhất.
-
Quản lý được số lượng thể hiện (instance) của một lớp (class) trong một giới hạn chỉ định.
Các nguyên tắc cơ bản để implement Singleton pattern
Chúng ta có rất nhiều cách để implement Singleton pattern. Nhưng dù cho việc implement bằng cách nào đi nữa cũng cần dựa vào những nguyên tắc dưới cơ bản dưới đây:
-
Dữ liệu instance (private và static) là đối tượng duy nhất của lớp Singleton.
-
Private hoặc protected constructor nhằm hạn chế người dùng tạo instance trực tiếp từ lớp (class) bên ngoài.
-
Đặt private static final variable để đảm bảo biến chỉ được khởi tạo trong class.
-
Có một method public static để return instance đã được khởi tạo ở trên.
9 cách để implement Singleton pattern phổ biến nhất
Dựa trên những nguyên tắc thiết kế Singleton trên, chúng ta có 9 cách implement Singleton pattern sau:
Eager initialization (Khởi tạo sớm)
Đối tượng Singleton Class sẽ tự động khởi tạo ngay khi ứng dụng bắt đầu chạy. Mặc dù đây là cách tiếp cận đơn giản và thuận tiện, nhưng nó mang theo một hạn chế quan trọng: trong trường hợp đã khởi tạo, đối tượng có thể không được sử dụng, dẫn đến sự lãng phí tài nguyên.
Ví dụ:
Eager initialization dễ cài đặt nhưng nó dễ dàng bị phá vỡ bởi Reflection
Static block initialization
Chúng ta làm tương tự như Eager initialization chỉ khác ở phần static block cung cấp thêm lựa chọn cho việc handle exception hay các xử lý khác.
Ví dụ:
Lazy Initialization
Lazy Initialization đã ra đời để khắc phục những hạn chế của Eager Initialization. Thay vì khởi tạo instance ngay từ đầu, Lazy Initialization chỉ thực hiện việc này khi được gọi. Điều này mang lại lợi ích là bạn không cần phải tạo class khi chưa cần sử dụng nó. Tuy nhiên, một điều cần lưu ý là nếu quá trình tạo instance diễn ra chậm, người dùng có thể phải đợi lâu cho lần sử dụng đầu tiên.
Mặc dù Lazy Initialization có những ưu điểm như trên, nhưng nó chỉ hoạt động hiệu quả trong trường hợp chương trình đơn luồng. Trong trường hợp có nhiều luồng (multi-thread) cùng truy cập vào phương thức getInstance() cùng một lúc, có khả năng xảy ra tình trạng hai thực thể (instance) được tạo ra đồng thời. Để giải quyết vấn đề này, phương pháp Thread Safe Singleton đã được tạo ra.
Thread Safe Initialization
Double Check Locking Singleton là một cách tiếp cận hiệu quả hơn so với Thread Safe Initialization, giúp khắc phục nhược điểm về hiệu năng. Thay vì sử dụng synchronized cho toàn bộ phương thức getInstance(), chúng ta chỉ sử dụng synchronized khi cần thiết, nghĩa là chỉ đồng bộ hóa khi instance của class chưa được khởi tạo.
Cụ thể, trong Double Check Locking Singleton, chúng ta kiểm tra instance của class trước khi thực hiện synchronized. Nếu instance đã được tạo, các luồng khác sẽ không phải đợi đến khi synchronized được mở ra. Điều này giảm thiểu thời gian chờ đợi và tăng hiệu suất chương trình.
Tuy nhiên, cần chú ý rằng việc triển khai Double Check Locking trong môi trường Java đôi khi có thể gặp vấn đề về xử lý bộ nhớ và thứ tự thực thi trên các hệ thống khác nhau. Để giải quyết vấn đề này, chúng ta có thể sử dụng từ khóa “volatile” để đảm bảo rằng biến instance được đọc từ bộ nhớ chính, không được đặt vào bộ nhớ cache của từng luồng.
Tóm lại, Double Check Locking Singleton là một phương pháp tối ưu hóa hiệu suất so với Thread Safe Initialization, giúp giảm thiểu tác động đến hiệu suất chương trình trong tình huống đa luồng mà vẫn đảm bảo tính thread-safe và duy nhất của instance.
Ví dụ của Thread Safe Initialization
Double Check Locking Singleton
Để triển khai mẫu Singleton theo phương pháp này, chúng ta sẽ thực hiện kiểm tra sự tồn tại của đối tượng instance trong lớp, và sử dụng đồng bộ hóa để đảm bảo rằng chỉ có hai lần kiểm tra trước khi khởi tạo được thực hiện. Để đảm bảo tính chính xác của quá trình biên dịch, cần khai báo từ khóa volatile cho đối tượng instance, nhằm tránh những vấn đề không chính xác trong quá trình tối ưu hóa của trình biên dịch.
Ví dụ về Double Check Locking Singleton
Bill Pugh Singleton Implementation
Bill Pugh Singleton implementation tạo ra static nested class với chức năng 1 Helper khi muốn tách biệt chức năng cho 1 class function rõ ràng hơn. Đây là cách thường được sử dụng nhiều và có hiệu suất tốt.
Ví dụ về Bill Pugh Singleton Implementation
Trong quá trình Singleton được nạp vào bộ nhớ, Singleton Helper vẫn chưa được nạp. Thay vào đó, Singleton Helper chỉ được nạp khi phương thức getInstance() được gọi. Cách tiếp cận này giúp tránh lỗi khởi tạo đối tượng Singleton trong môi trường Multi-Thread và cung cấp hiệu suất cao. Sự tách biệt trong quá trình xử lý giúp đảm bảo tính nhất quán. Nhờ vào đặc điểm này, phương pháp triển khai Singleton được đánh giá là một cách nhanh chóng và hiệu quả, được ưa chuộng bởi cộng đồng lập trình viên.
Reflection Singleton Pattern
Reflection có thể được dùng để phá vỡ Pattern của Eager Initialization và Bill Pugh Singleton như ví dụ dưới đây:
Ví dụ về Reflection Singleton Pattern
Khi đó Output của chương trình là:
Enum Singleton
Khi dùng Enum các params chỉ được khởi tạo 1 lần duy nhất. Đây cũng là cách để bạn tạo ra Singleton instance.
Ví dụ:
Lưu ý 2 điều sau đây khi dùng Enum Singleton:
-
Enum có thể sử dụng như một Singleton. Nhược điểm của nó là không thể extends từ một class được, nên khi sử dụng cần xem xét vấn đề này.
-
Hàm constructor của enum là có tính chất lazy, nghĩa là khi được sử dụng mới chạy hàm khởi tạo và nó chỉ chạy duy nhất một lần. Nếu bạn muốn sử dụng như một Eager Singleton thì cần gọi stance trong một static block khi bắt đầu chương trình.
Serialization and Singleton
Trong các môi trường hệ thống phân tán, việc triển khai giao diện Serializable trong lớp Singleton đôi khi là cần thiết. Điều này giúp chúng ta có khả năng lưu trữ trạng thái của đối tượng Singleton vào file hệ thống và khôi phục lại nó sau đó.
Ví dụ:
Đoạn code test quá trình Serialize/ Deserialize:
Output của chương trình là:
Như ở ví dụ trên, Deserialize đối tượng của Serialized Singleton khác với đối tượng gốc.
Nhưng nó sẽ không xảy ra khi bạn dùng phương pháp Enum.
Trên thực tế, vẫn có cách khắc phục khi sử dụng class Serialized Singleton là implement một phương thức readResolve(). Nhưng khi chúng ta thật sự gặp vấn đề và cần sử dụng Serialize/ Deserialize, thì nên sử dụng Enum sẽ đơn giản hơn.
Một số ứng dụng của Singleton Pattern
Chúng ta có thể thấy Singleton Pattern được ứng dụng trong các trường hợp:
-
Trường hợp giải quyết các bài toán cần truy cập vào các ứng dụng như: truy cập giao diện phần cứng, tệp cấu hình, Shared resource, Logger, Configuration, Caching, Thread pool ,…
Cụ thể:
Truy cập giao diện phần cứng: Singleton được sử dụng tùy thuộc vào yêu cầu. Các lớp Singleton cũng được sử dụng để ngăn chặn truy cập đồng thời của class. Trên thực tế, Singleton có thể được sử dụng trong trường hợp yêu cầu giới hạn sử dụng tài nguyên phần cứng bên ngoài. Chẳng hạn như: máy in phần cứng nơi mà bộ đệm in có thể được tạo thành một Singleton nhằm tránh nhiều truy cập đồng thời và tạo ra tắc nghẽn.
Logger: Các class Singleton được ứng dụng trong các tệp nhật ký. Các tệp nhật ký được tạo bởi đối tượng class trình ghi nhật ký. Ví dụ, một ứng dụng trong đó tiện ích ghi nhật ký phải tạo một tệp nhật ký dựa trên các thông báo nhận được từ người dùng. Nếu có nhiều ứng dụng khách sử dụng lớp tiện ích ghi nhật ký này, chúng có thể sẽ tạo nhiều bản sao của lớp này và có thể gây ra lỗi trong quá trình truy cập cùng lúc vào cùng một file trình ghi nhật ký. Chúng ta có thể dùng lớp tiện ích ghi nhật ký như một lớp đơn và cung cấp một điểm tham chiếu chung để mỗi người dùng có thể sử dụng tiện ích này và không có 2 người dùng nào truy cập nó cùng một lúc.
Tệp cấu hình: Singleton pattern rất phù hợp để ứng dụng vào tệp cấu hình vì nó ngăn cản nhiều người dùng truy cập liên tục và đọc tệp cấu hình hoặc tệp thuộc tính.
-
Một số design pattern sử dụng Singleton để thiết kế: Abstract Factory, Builder, Prototype, Facade,…
-
Sử dụng trong: java.lang.Runtime, java.awt.Desktop,..
Dưới đây là một số ứng dụng phổ biến của Singleton pattern. Tuy nhiên, không chỉ giới hạn ở đó, Singleton pattern còn có nhiều ứng dụng khác mà chúng ta có thể tận dụng.
Chúng tôi hi vọng rằng thông qua bài viết trên, bạn đã có cái nhìn rõ hơn về Singleton pattern là gì, chức năng của nó, cũng như các phương pháp thực thi và ứng dụng trong thực tế. Bằng cách này, bạn sẽ có khả năng áp dụng Singleton pattern một cách hiệu quả nhất trong công việc của mình!