Vài phút hiểu về Java Singleton Pattern

Java Singleton Pattern là gì thế? Liệu rằng vài phút có hiểu hết được Java Singleton Pattern? Nó có nhiều lắm không? Những câu hỏi này mình sẽ giải đáp cho các bạn khi đọc hết những nội dung bên dưới nhé :)))

Vài phút hiểu về Java Singleton Pattern

Trả lời cho câu hỏi Java Singleton Pattern là gì?

Singleton Pattern là một mẫu thiết kế (design pattern) có tên là singleton, thuộc nhóm Creational Design Pattern (nhóm này có 5 design pattern, singleton là 1 trong số 5 pattern đó).

Ngoài lề một tý trước khi hiểu tiếp về Singleton Pattern

Khi bạn định nghĩa một class (chẳng hạn như SinhVien), tại nhiều class khác (có thể là class SinhVienDemo1, SinhVienDemo2, QuanLySinhVien) bạn gọi đối tượng từ class SinhVien đó để sử dụng.

SinhVien sv1 = new SinhVien();
SinhVien sv2 = new SinhVien();
SinhVien sv3 = new SinhVien();

sv1, sv2, sv3 là 3 đối tượng thuộc class SinhVien, 3 đối tượng này là 3 thể hiện (instance) của class đó (đối tượng là thể hiện của class) và được gọi từ các class khác nhau. Các thể hiện này sẽ được trỏ đến các vùng nhớ khác nhau trong java virtual machine, đồng nghĩa với việc dữ liệu của 3 đối tượng sẽ khác nhau, không liên quan với nhau.

Nhưng đời đâu như là mơ, sếp mình lại bảo muốn có 3 đối tượng (3 tên đối tượng sv1, sv2, sv3), nhưng lại cùng trỏ đến một vùng nhớ trong java virtual machine thôi. Như vậy, chẳng khác gì cho 3 chàng trai cùng làm chồng của 1 cô gái (em ấy sướng quá rồi 😀 ).

Sao sếp lại làm chuyện ngược đời thế? Phải chăng chương trình chỉ quản lý 1 sinh viên duy nhất (lớp này ế nhỉ)

Nghe lời sếp, thế thì có giải pháp thế này:

Ở class SinhVienDemo1 bạn tạo ra đối tượng static sv1 với phạm vi truy cập public

public static SinhVien sv1 = new SinhVien();

Ở class SinhVienDemo2, QuanLySinhVien bạn chỉ cần tạo sv2, sv3 rồi nhận giá trị thôi

SinhVien sv2 = SinhVienDemo1.sv1;
SinhVien sv3 = SinhVienDemo1.sv1;

Hơi xàm rồi đấy, có nhiều cách để bạn thực hiện việc này, ở trên là 1 ví dụ. Tuy nhiên, với những cách thế này thì nhiều lúc sv2, sv3 bị dính NULL đó nhé, rồi tự nhiên muốn sử dụng SinhVien lại phải dùng SinhVienDemo1 làm class trung gian. Ôi! Cuộc đời sao bế tắc thế, ở nhà làm nông, chăn trâu chăn gà còn sướng hơn. 🙁

Được anh sếp giới thiệu giải pháp Singleton Pattern

Chưa cần biết cách triển khai Singleton nó thế nào, cứ xem ví dụ đã nhé.

Ở class SinhVien bạn tạo một phương thức static svduynhat() trả về đối tượng của chính class SinhVien (phương thức này có phạm vi truy cập là public)

private static SinhVien sv = null;

public static SinhVien svduynhat() {
    if (sv == null) {
        sv = new SinhVien();
    }
    return sv;
}

Rồi thì ở các class SinhVienDemo1, SinhVienDemo2, QuanLySinhVien chỉ cần tạo các đối tượng sv1, sv2, sv3 thế này

SinhVien sv1 = SinhVien.svduynhat();
SinhVien sv2 = SinhVien.svduynhat();
SinhVien sv3 = SinhVien.svduynhat();

Như vậy, khi dùng Singleton Pattern mà sếp hướng dẫn, bạn đã thấy sung sướng hơn chưa, từ bỏ trâu gà quay lại nghiệp Code nhé 😀

Lưu ý

Ví dụ trên chỉ code đơn giản theo hướng giải quyết của singleton pattern chứ chưa thật sự đầy đủ, bạn theo dõi những gì bên dưới rồi quay lại hoàn thành ví dụ nhé 😀

Vả lại, Mình chưa thấy một ứng dụng thực tế nào như ví dụ trên cả, tất cả chỉ là ví dụ để bạn hiểu singleton nó được áp dụng thế nào thôi 😀

Tiếp tục bàn về khái niệm Singleton Pattern

Từ ví dụ đơn giản trên, bạn có thể thấy Singleton Pattern giải quyết ổn thoả việc tạo ra một đối tượng (thể hiện) duy nhất từ một lớp để sử dụng trong suốt quá trình chương trình chạy.

Singleton Pattern nhiều lắm không?

Không rõ ở đây nhiều lắm là nhiều về cái gì? Tuy nhiên mình vẫn trả lời thế này.

Về mặt chức năng: Singleton đảm bảo rằng class chỉ có duy nhất một thể hiện (hay đối tượng) được tạo ra và nó sẽ cung cấp một phương thức để bạn truy cập đến thể hiện đó.

Về mặt nguyên tắc: Dù bạn thực hiện bằng cách nào đi chăng nữa thì cũng phải dựa vào các nguyên tắc sau:

  1. Constructor của class dùng Singleton Pattern phải có phạm vi truy cập là private để không thể tạo thể hiện của class từ bên ngoài.
  2. Khai báo private static cho tên biến của thể hiện đã tạo để đảm bảo thể hiện chỉ được tạo ra trong class đó thôi.
  3. Có một phương thức public để trả về thể hiện đã được tạo.

Như vậy chỉ cần nắm rõ chức năng và 3 nguyên tắc của singleton pattern là bạn có thể áp dụng vào project của mình, cách đặt tên thể hiện hay phương thức thì tuỳ bạn thôi. Vậy bạn thấy có nhiều không?

Đến đây thì thật sự không nhiều kiến thức về singleton pattern để bạn nhớ đâu, chỉ cần nhớ những điều trên là được. Tuy nhiên, bạn có đảm bảo rằng cách code của bạn theo nguyên lý singleton pattern thật sự hiệu quả và thực thi nhanh chóng. Không thể dám chắc đúng không? Vì bạn có thể không phải là một chuyên gia. 😀

Cách chuyên gia code theo nguyên lý singleton pattern

Đã gọi là chuyên gia thì ắt hẳn phải là một bậc thầy phải không các bạn. Vậy những cách áp dụng singleton của các bậc thầy như thế nào thì cùng mình điểm qua vài phương pháp chính sau nhé.

1. Eager initialization

Thể hiện của lớp sẽ được tạo ngay khi được gọi đến. Đây là cách dễ nhất nhưng nó có một nhược điểm là mặc dù thể hiện (instance) đã được khởi tạo nhưng có thể sẽ không dùng tớ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;
    }
}

2. Static block initialization

Cũng tương tự Eager initialization nhưng có thêm phần static block cung cấp thêm tuỳ chọn để xử lý việc khác (chẳng hạn như lỗi).

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;
    }
}

3. Lazy Initialization

Là một cách làm mang tính mở rộng hơn so với 2 cách làm trên, hoạt động tốt trong từng thread đơn lẻ. Và tất nhiên vấn đề xấu sẽ xảy ra nếu chúng ta đang dùng nó với multi thread. (Để khắc phục nhược điểm này, làm theo cách thứ 4).

public class LazyInitializedSingleton {

    private static LazyInitializedSingleton instance;

    private LazyInitializedSingleton(){}

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

4. Thread Safe Singleton

Sử dụng thêm từ khóa synchronized trong phương thức trả về thể hiện của lớp. (Dùng trong các chương trình multi thread)

public class ThreadSafeSingleton {

    private static ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton(){}
    
    public static synchronized ThreadSafeSingleton getInstance(){
        if(instance == null){
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
    
}

5. Bill Pugh Singleton Implementation

Với cách làm này sẽ tạo ra static nested class để tạo thể hiện (Cách mình hay dùng)

public class BillPughSingleton {

    private BillPughSingleton(){}

    private static class SingletonHelper{
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance(){
        return SingletonHelper.INSTANCE;
    }
}

6. Using Reflection to destroy Singleton Pattern

Reflection được dùng để destroy tất cả các singleton mà chúng ta đã tạo ra.

public class ReflectionSingletonTest {

    public static void main(String[] args) {
        EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
        EagerInitializedSingleton instanceTwo = null;
        try {
            Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
            for (Constructor constructor : constructors) {
                //Below code will destroy the singleton pattern
                constructor.setAccessible(true);
                instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println(instanceOne.hashCode());
        System.out.println(instanceTwo.hashCode());
    }

}

7. Enum Singleton

Nếu như bạn đã đọc qua cuốn Effective Java sẽ thấy Joshua Bloch đã khẳng định rằng “sử dụng enum type là cách tốt nhất để triển khai Singleton” cho bất kì ngôn ngữ nào mà hỗ trợ enums. (Tuy nhiên mình không tin cho lắm 😀 )

public enum EnumSingleton {

    INSTANCE;

    public static void doSomething(){
        //do something
    }
}

8. Serialization and Singleton

Serialization là kỹ thuật sắp xếp đối tượng cần lưu trữ một cách tuần tự. Ví dụ bên dưới là quá trình đọc ghi dữ liệu tích hợp Singleton.

public class SerializableSingletonClass implements Serializable{
    private static final long serialVersionUID = 1L;
    private int value;
    private String name;

    private SerializableSingletonClass(int value, String name) {
        if( value < 0 ) throw new IllegalArgumentException("Value may not be less than 0");
        this.value = value;
        this.name = Validate.notNull(name, "Name may not be null");
    }

    private static class SerializableSingletonHolder{
        public static final SerializableSingletonClass INSTANCE;
        static {
            INSTANCE = new SerializableSingletonClass(0, "default");
        }
    }

    private void readObject(ObjectInputStream stream) throws InvalidObjectException{
        throw new InvalidObjectException("proxy required");
    }

    private Object writeReplace(){
        return new SerializationProxy(this);
    }

    private static class SerializationProxy implements Serializable{
        private static final long serialVersionUID = 1L;
        public SerializationProxy(SerializableSingletonClass ignored) { } //Here is the question

        private Object readResolve(){
            return SerializableSingletonHolder.INSTANCE;
        }
    }
}

Còn đây là bảng đo hiệu suất khi sử dụng các phương pháp này

Vài phút hiểu về Java Singleton Pattern

Có thể thấy cách làm khi sử dụng inner-class static (hay Bill Pugh Singleton Implementation) đạt hiệu suất cao nhất.

Như vậy, có nhiều chuyên gia, có nhiều cách thực hiện. Có cách tốt, cách tệ, cách có hiệu suất cao, cách có hiệu suất thấp, cách dùng tốt với single thread, cách dùng tốt với multi thread,…nên bạn phải nhớ rằng bản chất mới là quan trọng, cách thức thực hiện chẳng là gì cả. Ở một số dự án bạn thực hiện theo phương pháp này, nhưng một số dự án bạn bắt buộc phải thực hiện theo một phương pháp khác. Các phương pháp trên của các chuyên gia đã được kiểm chứng và sử dụng trong nhiều dự án, bạn có thể sử dụng một trong những phương pháp đó nhé. (Có thể chọn cách Bill Pugh Singleton Implementation vì bạn cũng thấy hiệu suất của nó rồi đó 😀 )

Ứng dụng của Singleton Pattern

Bạn thử hình dung trong đầu mình rằng đã hiểu về Singleton Pattern như thế nào rồi nhé. Cứ việc tập trung vào chức năng của nó: “đảm bảo rằng class chỉ có duy nhất một thể hiện (hay đối tượng) được tạo ra và nó sẽ cung cấp một phương thức để bạn truy cập đến thể hiện đó” để có thể ứng dụng vào trong các dự án mà bạn cần đến.

Dưới đây là một số ứng dụng của singleton pattern để bạn tham khảo thêm:

  • Vì class dùng Singleton chỉ tồn tại 1 Instance (thể hiện) nên nó thường được dùng cho các trường hợp giải quyết các bài toán cần truy cập vào các shared resource hoặc implement cho các Logger class (logging), Configuration class, DAO, drivers objects, caching hoặc thread pool.
  • Một số design pattern khác cũng sử dụng Singleton để triển khai: Abstract Factory, Builder, Prototype, Facade,…
  • Singleton Pattern cũng được sử dụng trong một số class của core java, ví dụ như: java.lang.Runtime, java.awt.Desktop.

Sếp nói thêm

À thật ra thì không phải sếp nói là mình tóm lại thôi nhé. 😀

Đó là tất cả những gì mình hiểu, tìm hiểu và vận dụng Singleton Pattern. Mình khuyên các bạn chỉ cần hiểu đơn giản thôi, đừng quan trọng hoá vấn đề.

Phương pháp Singleton mình hay sử dụng là Bill Pugh Singleton Implementation (vì nó có hiệu suất cao), Lazy Initialization (vì tính đơn giản của nó nếu không dùng đến multi thread) và Thread Safe Singleton (tuy hiệu suất thấp nhưng là cách khá an toàn khi có dùng multi thread). Code ví dụ thì đã có ở trên, các bạn xem lại nhé.

Cuối cùng, hẹn gặp lại các bạn ở những bài viết phân tích sâu hơn về từng phương pháp của các chuyên gia để thấy ưu nhược điểm của từng cái và các ví dụ mẫu cho từng phương pháp đó.

Nếu có vấn đề gì cần thảo luận thì để lại bình luận bên dưới bạn nhé <3

5 1 vote
Đánh giá bài viết
Nhận thông báo
Thông báo khi có
guest

0 Bình luận
Phản hồi nội tuyến
Xem tất cả các bình luận