Mở đầu series về Design Pattern, chúng ta sẽ bắt đầu đi từ những pattern đơn giản nhất, phổ biến nhất mà tất cả lập trình viên Java cần phải nắm cực kì rõ. Và Pattern đầu tiên trong số đó là Singleton :smile:.
Vậy Singleton Pattern là gì?
Đối với các môn sinh tìm hiểu về Design Pattern, Singleton có lẽ là pattern thuộc dạng dễ hiểu nhất, dễ thực hiện nhất. Tuy nhiên để áp dụng đúng cách, đúng trường hợp thì lại không đơn giản. Ờm nói cho nguy hiểm vậy thôi chứ cũng k đến nỗi lằng nhằng lắm, đại khái là có 1 số trường hợp chúng ta cần phải chú ý để có thể tối ưu được việc sử dụng Singleton trong project. 🙂
Định nghĩa
Nếu bạn đã hiểu khái niệm về Instance thì việc hiểu công dụng của Singleton cũng không gặp nhiều khó khăn.
Đại khái là, Singleton đảm bảo sẽ chỉ có duy nhất 1 Instance của class được khởi tạo và sử dụng trên máy ảo JVM.
Sound good? Công dụng nó chỉ có vậy thoai 🙂
Tiếp theo, các quy định cơ bản chúng ta phải tuân thủ khi sử dụng Singleton:
Constructor
của class phải luôn làprivate
method
, nhằm hạn chế việc khởi tạo ở các class khác.- Các variable được khởi tạo và sử dụng cùng
constructor
phải luôn làprivate
static
variable
. - Luôn phải có 1
public
method
phục vụ cho việc trả lại Instance của class, đây sẽ là method duy nhất mà các class khác có thể sử dụng để khởi tạo class sử dụng Singleton.
Hừm, vẫn còn thấy khó hiểu? Đang ngồi lẩm bẩm sao thằng tác giả nói gì mà lằng nhằng thế? Okidoki, vậy bây giờ ta sẽ đi vào từng ví dụ cụ thể, để hiểu cách sử dụng của Singleton trong từng trường hợp nhé, để đỡ phải ngồi chửi thầm…
Cách sử dụng
Nói thẳng ra là mình cũng chẳng sử dụng hết các cách này đâu, cũng tham khảo 1 số nguồn và tổng hợp lại cho mọi người thôi. 🙂
Singleton mình liệt kê ở đây sẽ có 4 cách cơ bản và phổ biến nhất (Sử dụng tên tiếng Anh cho mọi người dễ search):
Ngoài ra còn mấy cách tricky kiểu như sử dụng Enum thì cho qua đi, khó nhớ lắm…
1. Eager initialization
Cách đầu tiên cũng là cách sơ đẳng, đơn giản cmn nhất, đúng kiểu đọc định nghĩa phát có thể viết được luôn rồi.
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
//private constructor to avoid client applications to use constructor
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance(){
return instance;
}
}
Quá đơn giản phải không ạ? Áp dụng đúng theo các qui định ở trên là xong, có biến private
nè, có private
constructor
nè, có private
method
để lấy Instance nè. Ờ nhưng mà cách này thì độ thực tế không cao lắm, à mà có thể cũng cao, vì nhiều ông dev lười để ý, cứ phệt toẹt như sách vào thế này… Lí do vì sao mà mềnh nói lợi bất cập hại, đơn giản là vì… vì… Java và JVM nó tồn tại 1 cái gọi là Class loading…
Tức là, Ứng dụng trước khi hoạt động được, JVM sẽ tải hết thông tin về Class vào bộ nhớ (bao gồm cả name, method, variable đi kèm blo bla…).
Nếu sử dụng cách ở trên, Instance của Class sẽ được khởi tạo sẵn và nạp vào bộ nhớ cùng vs Class. Quá nguy hiểm nếu Instance có dung lượng quá lớn, nếu trong quá trình sử dụng app mà ta không sử dụng đến Instance này thì đấy quả là 1 sự lãng phí k cần thiết…
Chưa hết, vẫn còn 1 điểm hại nữa… Điều gì xảy ra nếu việc khởi tạo Instance gặp vấn đề, hay còn gọi là Exception? Đúng rồi, bật app lên cái chết nhe răng chứ còn gì nữa… :sob:
Thế này cách này chỉ để cho biết thôi, chứ dùng thì vứt, vứt nhé…
2. Static block initialization
Cách này thông minh hơn cách 1 một tí, đó là có thể xử lý Exception! Wohooo!!! :ok_hand:
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
//static block initialization for exception handling
static{
try{
instance = new StaticBlockSingleton();
}catch(Exception e){
throw new RuntimeException("Exception occured in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance(){
return instance;
}
}
1 cách sử dụng khá là lạ, static
block trong class, Mình cũng chẳng mấy khi sử dụng syntax này trong Java…
Tuy nhiên nó vẫn chưa giải quyết được vấn đề về tối ưu hoá sử dụng bộ nhớ… Next thôi next thôi.
3. Lazy Initialization
Khái niệm Lazy… được sử dụng rất nhiều trong trong lập trình, hiểu nôm na là:
Tôi sẽ tạo 1 cái Instance rỗng xong để đấy làm đại diện thôi, bao giờ dùng thì mới cấp bộ nhớ.
Đấy, lười chưa, tạo thì cấp cho luôn đi, lại còn phải khi nào dùng ms cấp cơ…
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance(){
if(instance == null){
instance = new LazyInitializedSingleton();
}
return instance;
}
}
Cách này nghe có vẻ hợp lý rồi đấy, k còn phải lăn tăn việc Exception hay tối ưu bộ nhớ nữa. Tuy nhiên thịt chó hay ăn với lá mơ, và đời cũng không như là mơ. Phương pháp này trong 1 số trường hợp sẽ là điểm yếu chết người. Đó là khi được sử dụng trong Distributed system, hay gọn hơn là trong Multi-thread system.
Lí do là sao, là đây:
Điều gì xảy ra khi 2 Thread riêng biệt cùng gọi method
getInstance
và check được rằnginstance == null
trả vềtrue
. Lúc đấyconstructor
sẽ bị gọi 2 lần (hoặc tệ hơn là n lần nếu có n Thread). Và thế là quy tắc về Instance sẽ bị phá vỡ.
Vậy chúng ta phải làm gì? Đến với cách 4 chứ làm gì…
4. Thread Safe Singleton
Phương pháp này sẽ giải quyết bài toán bằng cách:
Cho lần lượt từng Thread truy cập vào method
getInstance()
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static synchronized ThreadSafeSingleton getInstance(){
if(instance == null){
instance = new ThreadSafeSingleton();
}
return instance;
}
}
thêm synchronized
vào, thế là ta đã giải quyết xong. Việc cho synchronized
sẽ đảm bảo trong 1 thời điểm sẽ chỉ có 1 Thread được phép thực thi method này.
…
…
…
Ahihi, bạn tin người vcd… Nghĩ gì mà lại đi dùng cách đấy… Chẳng lẽ mỗi gần gọi Instance chúng ta đều phải đưa về chế độ synchronization? Performance có vẻ không ổn lắm…
Dùng cách này này:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
if(instance == null){
synchronized (ThreadSafeSingleton.class) {
if(instance == null){
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Phương pháp này được gọi là double checked locking (Google bảo thế…). Nguyên tắc rất cơ bản:
Với trường hợp Instance chưa được tạo, tất cả Thread sẽ phải chờ 1 Thread đến trước nhất khởi tạo xong Instance, sau đó sử dụng như bình thường.
Với trường hợp Instance đã được tạo, không cần phải đưa về synchronization.
Giờ thì có lẽ ok rồi đấy.
Kết thúc bài viết đầu tiên. Nếu bạn cảm thấy mình viết vẫn còn khó hiểu, có thể liên hệ trực tiếp qua Facebook hoặc Email của mềnh (Mọi thức đã được đính kèm ở đâu đấy trong web :see_no_evil:)
Happy Coding!
Reference: Internet
Bạn làm tốt lắm, bài này của bạn đã giải thích rõ hơn bên trang
https://ngockhuong.com/java/java-design-pattern/vai-phut-hieu-ve-java-singleton-pattern.html
Mình chỉ bổ sung thêm là cái double checked locking thì bạn còn phải đổi
private static ThreadSafeSingleton instance;
thành private volatile static ThreadSafeSingleton instance;
Nếu không làm vậy thì có nghĩa bạn đang lãng phí memory của hệ thống cho cơ chế synchronize không cần thiết, và lúc này thread sẽ bị block lại, nếu bạn đã từng thử điều này thì bạn sẽ thấy hệ thống của bạn bị lag.