[Design Pattern] Singleton và cách sử dụng trong Java

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):

  1. Eager initialization
  2. Static block initialization
  3. Lazy Initialization
  4. Thread Safe Singleton

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ì… JavaJVM 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ằng instance == null trả về true. Lúc đấy constructor 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

Leave a Reply