Singleton Pattern trong Java

Singleton Class trong Java

1. Giới thiệu

Trong bài viết ngắn này, chúng tôi sẽ thảo luận về hai cách phổ biến nhất để implement Singletons trong Java thuần.

2. Singleton dựa trên lớp

Cách tiếp cận phổ biến nhất là implement Singleton bằng cách tạo một class bình thường và đảm bảo nó có:

  • Một private constructor
  • Một trường static chứa instance duy nhất của class
  • Một static factory method để get instance

Chúng ta cũng sẽ thêm một thuộc tính thông tin (sẽ để sử dụng sau). Vì vậy, implementation của chúng ta sẽ trông như thế này:

public final class ClassSingleton {
 
    private static ClassSingleton INSTANCE;
    private String info = "Initial info class";
     
    private ClassSingleton() {        
    }
     
    public static ClassSingleton getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new ClassSingleton();
        }
         
        return INSTANCE;
    }
 
    // getters and setters
}

Mặc dù đây là một cách tiếp cận phổ biến, nhưng điều quan trọng cần lưu ý là nó có thể có vấn đề trong các tình huống có sử dụng đa luồng (mà đa luồng lại là mục đích chính khi sử dụng Singletons).

Nói một cách đơn giản, nó có thể dẫn đến nhiều hơn một instance, phá vỡ nguyên tắc cốt lõi của pattern này. Mặc dù có các giải pháp lock cho vấn đề này, phương pháp tiếp theo của chúng tôi giải quyết những vấn đề này ở cấp độ gốc rễ.

3. Enum Singleton

Tiếp theo, chúng ta hãy thảo luận về một cách tiếp cận thú vị khác - đó là sử dụng enumerations

public enum EnumSingleton {
     
    INSTANCE("Initial class info"); 
  
    private String info;
  
    private EnumSingleton(String info) {
        this.info = info;
    }
  
    public EnumSingleton getInstance() {
        return INSTANCE;
    }
     
    // getters and setters
}

Cách tiếp cận này có serialization và thread-safety được đảm bảo bởi bản thân implementation của enum, điều này đảm bảo bên trong chỉ có một instance duy nhất và khắc phục các vấn đề trong implementation dựa trên class.

4. Cách sử dụng

Để sử dụng ClassSingleton, chúng ta chỉ cần lấy instance bằng static method:

ClassSingleton classSingleton1 = ClassSingleton.getInstance();
 
System.out.println(classSingleton1.getInfo()); //Initial class info
 
ClassSingleton classSingleton2 = ClassSingleton.getInstance();
classSingleton2.setInfo("New class info");
 
System.out.println(classSingleton1.getInfo()); //New class info
System.out.println(classSingleton2.getInfo()); //New class info

Còn với EnumSingleton thì chúng ta có thể sử dụng như bất cứ Java Enum nào khác

EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE.getInstance();
 
System.out.println(enumSingleton1.getInfo()); //Initial enum info
 
EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE.getInstance();
enumSingleton2.setInfo("New enum info");
 
System.out.println(enumSingleton1.getInfo()); // New enum info
System.out.println(enumSingleton2.getInfo()); // New enum info

5. Các lỗi thường gặp

Singleton là một design pattern đơn giản và có một vài lỗi phổ biến mà một lập trình viên có thể phạm phải khi tạo ra một singleton.

Chúng tôi phân biệt hai loại vấn đề với singletons:

  • tồn tại (chúng ta có cần một singleton không?)
  • triển khai (chúng ta có implement đúng không?)

5.1. Các vấn đề tồn tại

Về mặt khái niệm, singleton là một loại biến global. Nói chung, chúng ta biết rằng nên tránh các biến global - đặc biệt là nếu chúng không phải là immutable. Chúng tôi không nói rằng chúng ta không bao giờ nên sử dụng singletons. Tuy nhiên, chúng tôi đang nói rằng có thể có nhiều cách hiệu quả hơn để tổ chức code. Nếu việc implement một phương thức phụ thuộc vào một đối tượng đơn lẻ, tại sao không truyền nó làm tham số? Làm như thế, chúng ta thể hiện rõ ràng phương thức phụ thuộc vào cái gì. Do đó, chúng ta có thể dễ dàng làm giả những dependencies này (nếu cần) khi thực hiện test. Ví dụ: Singletons thường được sử dụng để bao bọc dữ liệu config của ứng dụng (tức là, kết nối với repository). Nếu chúng được sử dụng như global object, việc chọn cấu hình cho môi trường thử nghiệm trở nên khó khăn. Do đó, khi chúng ta chạy test, dữ liệu test sẽ vào trong production DB, và điều này khó có thể chấp nhận được. Nếu chúng ta cần một singleton, chúng ta có thể xem xét khả năng ủy thác việc khởi tạo của nó cho một class khác, một loại factory, để đảm bảo rằng chỉ có một instance của singleton.

5.2. Vấn đề triển khai

Mặc dù các singletons có vẻ khá đơn giản, việc implement có thể gặp phải nhiều vấn đề khác nhau. Tất cả đều dẫn đến việc là chúng ta có thể sẽ có nhiều hơn một instance của class.

Đồng bộ hóa Việc triển khai với một hàm tạo riêng mà chúng tôi đã trình bày ở trên không thread-safe: nó hoạt động tốt trong môi trường đơn luồng, nhưng trong môi trường đa luồng, chúng ta nên sử dụng kỹ thuật đồng bộ hóa để đảm bảo tính atomic của hoạt động:

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

Lưu ý từ khóa synchronized khi khai báo phương thức. Phần thân của phương thức có một số thao tác (so sánh, khởi tạo và trả về).

Trong trường hợp không có synchronized, có khả năng hai luồng thực thi xen kẽ, làm cho việc so sánh INSTANCE == null trả về true cho cả hai luồng và kết quả là hai instance ClassSingleton được tạo.

Đồng bộ hóa có thể ảnh hưởng đáng kể đến hiệu suất. Nếu mã này được gọi thường xuyên, chúng ta nên tăng tốc bằng cách sử dụng các kỹ thuật khác nhau như lazy initialization hoặc double-checked locking (lưu ý rằng cách này có thể không hoạt động như mong đợi do việc tối ưu hóa của trình biên dịch). Các bạn có thể xem chi tiết trong bài viết “Double-Checked Locking with Singleton“.

Nhiều instance Có một số vấn đề khác với các singletons liên quan đến bản thân JVM mà có thể gây ra việc sinh ra nhiều nhiều phiên bản của một singleton. Các vấn đề này khá subtle và chúng tôi sẽ đưa ra một mô tả ngắn gọn cho từng vấn đề:

Một singleton được coi là duy nhất cho mỗi JVM. Đây có thể là một vấn đề đối với các hệ thống phân tán hoặc hệ thống có nội bộ dựa trên các công nghệ phân tán. Mỗi class loader có thể load một phiên bản riêng của một singleton. Một singleton có thể được garbage-collected một khi không có gì tham chiếu đến nó. Vấn đề này không dẫn đến sự hiện diện của nhiều instance tại cùng một thời điểm, nhưng khi được tạo lại, phiên bản đó có thể khác với phiên bản trước đó.

6. Kết luận

Trong hướng dẫn nhanh này, chúng tôi đã tập trung vào cách triển khai Singleton pattern chỉ sử dụng Java thuần, đảm bảo tính nhất quán và cách sử dụng các implementation này.

Implementation đầy đủ cho các ví dụ này có thể xem trên GitHub.

Nguồn: Baeldung https://www.baeldung.com/java-singleton