Header Ads

Seo Services

Hế lô anh em ✌️✌️✌️

Tiếp tục chủ đề liên quan đến các Design Patterns, hôm nay mình sẽ cùng anh em tìm hiểu về Singleton Pattern - một pattern nữa thuộc nhóm các Creational Design Patterns.

Ngoài ra nếu anh em nào quan tâm thì có thể tham khảo các bài viết khác cùng chủ đề Design Patterns được mình tổng hợp tại đây nhé.

Ồ kê, cùng mình bắt đầu bài viết hôm nay nào!

1. Singleton là gì?

Vẫn là câu chuyện liên quan tới việc khởi tạo các đối tượng, Singleton được định nghĩa là một pattern giúp chúng ta đảm bảo một class chỉ có một thể hiện (instance) duy nhất của nó trong khi chúng ta vẫn có khả năng để truy cập toàn cục tới thể hiện này.

Định nghĩa được mình trích từ trang https://refactoring.guru/

"Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance"

Vậy cụ thể Singleton giải quyết những gì?

1️⃣ Thứ nhất, đó là câu chuyện về việc đảm bảo một class chỉ có duy nhất một thể hiện của nó. Vậy tại sao chúng ta lại muốn như vậy?

Đơn cử, mỗi khi tạo ra một thể hiện của class chúng ta phải cấp thêm tài nguyên bộ nhớ để lưu trữ thể hiển đó dù ở hình thức nào đi chăng nữa.

Ví dụ thường gặp nhất chính là việc truy cập đến các đối tượng thao tác với cơ sở dữ liệu, files hoặc các tài nguyên được chia sẻ (shared resource) chung trong hệ thống.

Hãy tưởng tượng chúng ta có một class là DatabaseConnection có chức năng kết nối tới database và thực hiện các công việc cập nhật thông tin vào database.

Bây giờ nếu mình chỉ có 1 chức năng đòi hỏi việc kết nối đến database thì có thể khởi tạo như sau: 

DatabaseConnection dbConnection = new DatabaseConnection()

Tất nhiên vấn đề sẽ không quá lớn, nhưng nếu mình có n chức năng muốn truy cập vào database thì sao? Chưa kể n chức năng mỗi chức năng lại truy cập m lần, dẫn đến có thể có n*m truy cập và điều này thực sự gây tốn kém tài nguyên bộ nhớ.

Rất may là Singleton sẽ giúp chúng ta giải quyết vấn đề này!

2️⃣ Tiếp theo, nếu đã giải quyết được câu chuyện chỉ tạo một instance duy nhất thì chúng ta sẽ gặp vấn đề trong việc làm sao truy cập tới instance đó (ở cấp độ toàn bộ chương trình) vì lúc này không phải cứ thích là new được!

Nhưng cũng rất may là Singleton sẽ giúp chúng ta giải quyết nốt vấn đề này!

2. Các bước triển khai Singleton

Để triển khai Singleton Pattern thường sẽ có hai bước:

1️⃣ Đầu tiên là tạo một constructor mặc định có chế độ truy cập private. Mục đích là để ngăn việc khởi tạo đối tượng từ bên ngoài class đó thông qua từ toán tử new.

2️⃣ Tiếp theo là tạo một static method, hàm này sẽ gọi đến private constructor để tạo một đối tượng và chúng ta sẽ gán nó vào một static field của class. Các lần gọi hàm này tiếp theo đều sẽ trả về đối tượng được tạo ra sau lần gọi đầu tiên (cached object)

Khi đó, ở bất cứ đâu trong chương trình nếu có thể sử dụng Singleton class này thì chúng ta đều có thể gọi tới static method và hàm này sẽ luôn luôn trả về cùng một đối tượng.

3. Những cách nào để triển khai Singleton Pattern?

Về cơ bản triển khai Singleton Pattern sẽ có hai bước như mình giới thiệu ở phần 2 nhưng trong thực tế lại có khá nhiều cách triển khai khác nhau để đáp ứng được các yêu cầu cụ thể.

Bây giờ mình sẽ cùng anh em tìm hiểu cụ thể những cách đó.

3.1 - Eager initialization

public class EagerInitializedSingleton {
    private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();

    // tránh việc client khởi tạo đối tượng từ bên ngoài class
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance(){
        return INSTANCE;
    }
}

Đây là cách triển khai đơn giản nhưng nó lại có một nhược điểm đó là instance sẽ được khởi tạo ngay khi class được gọi đến mà chưa cần phải gọi hàm getInstance()

Cụ thể nếu anh em thêm một hàm getStringOfInstance() như bên dưới thì khi gọi hàm này instance của chúng ta đã được tạo ra rồi mà chưa cần gọi đến hàm getInstance()

public class EagerInitializedSingleton {
    private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();

    // tránh việc client khởi tạo đối tượng từ bên ngoài class
    private EagerInitializedSingleton(){}

    public static EagerInitializedSingleton getInstance(){
        return INSTANCE;
    }

    public static String getStringOfInstance() {
        return INSTANCE.toString();
    }
}

Chính điều này có thể sẽ gây lãng phí bộ nhớ khi mà chưa cần nhưng đối tượng vẫn được khởi tạo. Ngoài ra cách này cũng không đảm bảo trong việc xử lý các ngoại lệ (exception handling)

3.2 - Static block initialization

public class StaticBlockInitializedSingleton {
    private static StaticBlockInitializedSingleton INSTANCE;

    private StaticBlockInitializedSingleton() {}

    static {
        try {
            INSTANCE = new StaticBlockInitializedSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }

    public static StaticBlockInitializedSingleton getInstance() {
        return INSTANCE;
    }
}

Thực ra cách triển khai này cũng chính là cách triển khai ban đầu chỉ có điều chúng ta sử dụng một static block thay vì toán tử new để có thể đảm bảo việc xử lý các ngoại lệ trong quá trình khởi tạo đối tượng.

3.3 - Lazy initialization

public class LazyInitializedSingleton {
    private static LazyInitializedSingleton INSTANCE;

    private LazyInitializedSingleton(){}

    public static LazyInitializedSingleton getInstance(){
        if(INSTANCE == null){
            INSTANCE = new LazyInitializedSingleton();
        }
        return INSTANCE;
    }
}

Với cách triển khai này chúng ta khắc phục được nhược điểm lớn nhất từ hai cách khởi tạo ban đầu đó là việc đối tượng sẽ không được khởi tạo ngay khi gọi đến class mà chỉ khi gọi hàm getInstance() thì đối tượng mới được khởi tạo một lần và duy nhất.

Nhưng điều này chỉ đúng trong môi trường đơn luồng (single-threaded) còn trong môi trường đa luồng (multi threaded) thì lại sai. 

Nguyên nhân là do tại cùng một thời điểm mỗi một luồng trong chương trình hoàn toàn có thể tạo ra một đối tượng mới vì điểu kiện if (INSTANCE == null) đều có thể đúng.

3.4 - Thread Safe Singleton

public class ThreadSafeInitializedSingleton {
    private static volatile ThreadSafeInitializedSingleton INSTANCE;

    private ThreadSafeInitializedSingleton() {}

    public static synchronized ThreadSafeInitializedSingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new ThreadSafeInitializedSingleton();
        }
        return INSTANCE;
    }
}

Note: Sử dụng volatile sẽ giúp thông báo sự thay đổi của một biến tới các luồng khác nhau trong chương trình nếu như biến này đang được sử dụng ở nhiều luồng khác nhau.

Để khắc phục vấn đề liên quan đến môi trường đa luồng chúng ta sẽ sử dụng synchronized method cho việc khởi tạo đối tượng.

Bản chất của cách làm này là đồng bộ hóa để các luồng chạy tuần tự khi có yêu cầu khởi tạo đối tượng. Ví dụ chương trình của mình có n luồng đang chạy thì có k (k <= n) luồng yêu cầu khởi tạo đối tượng.

Khi đó k luồng này sẽ tuần tự gọi đến hàm getInstance() và chỉ có luồng đầu tiên là khi gọi thì đối tượng được khởi tạo. Sau khi luồng đầu tiên gọi và khởi tạo thành công các luồng khác gọi vào hàm này sẽ được trả về cùng đối tượng được tạo ra từ lần gọi đầu tiên.  

Có thể thấy cách này giải quyết được vấn đề đa luồng nhưng về mặt hiệu năng (performance) thì lại không tốt vì các luồng sẽ phải chờ nhau và nó đánh mất ưu điểm của tính đa luồng.

Cải thiện điều này chúng ta có thể sử dụng Double Checked Locking Singleton như sau:

public class ThreadSafeInitializedSingleton {
    private static volatile ThreadSafeInitializedSingleton INSTANCE;

    private ThreadSafeInitializedSingleton() {}

    public static ThreadSafeInitializedSingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (ThreadSafeInitializedSingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new ThreadSafeInitializedSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

3.5 - Bill Pugh Singleton

public class BillPughInitializedSingleton {
    private BillPughInitializedSingleton() {}

    private static class SingletonHolder {
        static final BillPughInitializedSingleton INSTANCE = new BillPughInitializedSingleton();
    }

    public static BillPughInitializedSingleton getInstance() {
        return SingletonHolder.INSTANCE;
} }

Tên của cách khởi tạo này nghe "lạ" nhỉ! Thực ra đây là tên của người đã nghĩ cách này hay chính xác thì là khởi tạo theo kiểu initialization-on-demand-holder

Vậy tư tưởng của cách này là gì mà các chuyên gia đều khuyên sử dụng?

Không giống như những cách trước chúng ta tạo một static field bên trong class và khi class được gọi đến thì biến này sẽ được khởi tạo.

Khi sử dụng Bill Pugh Singleton chúng ta đẩy việc này cho một class khác đóng vai trò holder và khi class BillPughInitializeSingleton được gọi do không có bất kỳ static field nào nên sẽ không có bất kỳ đối tượng nào được khởi tạo.

Chỉ khi nào chúng ta gọi đến hàm getInstance() thì class SingletonHolder mới được gọi và static field bên trong lớp đó mới được khởi tạo bằng cách gọi đến private constructor của class  BillPughInitializeSingleton.

Việc khởi tạo là tuần tự và được đảm bảo bởi JLS (Java Language Specification). Cũng chính về thế chúng ta không cần phải đồng bộ phương thức này ngay cả trong môi trường đa luồng.

Đây là cách tiếp cận được đánh giá đơn giản nhưng lại khắc phục được hầu hết các vấn đề chúng ta phải đối mặt khi sử dụng những cách cũ (từ việc tránh khởi tạo khi không cần thiết, đảm bảo xử lý ngoại lệ cho đến các vấn đề về đa luồng)

4. Singleton Pattern được dùng khi nào và ở đâu?

Khi nào?

Như mình đã chia sẻ trong phần đầu và đặc điểm của các Singleton class là chỉ có một instance duy nhất nên chủ yếu pattern này được sử dụng trong việc giải quyết các bài toán truy cập tài nguyên như: Shared resource, Logger, Configuration, Caching, Thread pool...

Ở đâu?

Abstract Factory, Builder, Prototype... cũng có thể sử dụng Singleton để triển khai và tối ưu

Ngoài ra nhiều class như java.lang.Runtime, java.awt.Desktop cũng được triển khai dựa vào pattern này.

5. Lời kết

Singleton Pattern tuy là một pattern có cách triển khai đơn giản nhưng lại có nhiều cách triển khai khác nhau. Tùy trường hợp mà chúng ta sử dụng những cách triển khai sao cho phù hợp.

Đơn giản nhất anh em có thể sử dụng Bill Pugh Singleton vì hiệu suất cao được nhiều chuyên gia khuyên dùng.

Nhưng đôi khi với các ứng dụng đơn luồng anh em có thể dùng Lazy Initilized Singleton hoặc với các ứng dụng đa luồng anh em có thể sử dụng Double Checked Locking Singleton.

Tham khảo:

https://refactoring.guru/design-patterns/singleton

https://www.journaldev.com/1377/java-singleton-design-pattern-best-practices-examples#enum-singleton

https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom

https://gpcoder.com/4190-huong-dan-java-design-pattern-singleton/

Hẹn gặp lại anh em trong các bài viết tiếp theo nhé! Byee!👋👋👋 

4 nhận xét:

Được tạo bởi Blogger.